Skip to content

Commit

Permalink
Local storage implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
pdeziel committed Oct 25, 2023
1 parent 8e76799 commit 9e6e020
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 0 deletions.
7 changes: 7 additions & 0 deletions pkg/store/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package store

import "errors"

var (
ErrNotFound = errors.New("resource not found in store")
)
141 changes: 141 additions & 0 deletions pkg/store/local/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package local

import (
"archive/zip"
"io"
"os"
"path/filepath"
"sync"

"github.com/trisacrypto/courier/pkg/config"
"github.com/trisacrypto/courier/pkg/store"
)

const (
passwordPrefix = "password"
passwordFile = "password.txt"
certificatePrefix = "certificate"
certificateFile = "certificate.pem"
)

// Open the local storage backend.
func Open(conf config.LocalStorageConfig) (store *Store, err error) {
store = &Store{
path: conf.Path,
}

// Ensure the path exists
if err = os.MkdirAll(conf.Path, 0755); err != nil {
return nil, err
}

return store, nil
}

// Store implements the store.Store interface for local storage.
type Store struct {
sync.RWMutex
path string
}

var _ store.Store = &Store{}

// Close the local storage backend.
func (s *Store) Close() error {
return nil
}

//===========================================================================
// Password Methods
//===========================================================================

// GetPassword retrieves a password by id from the local storage backend.
func (s *Store) GetPassword(id string) (password []byte, err error) {
s.RLock()
defer s.RUnlock()
return s.load(s.fullPath(passwordPrefix, id))
}

// UpdatePassword updates a password by id in the local storage backend. If the
// password does not exist, it is created. Otherwise, it is overwritten.
func (s *Store) UpdatePassword(id string, password []byte) (err error) {
s.Lock()
defer s.Unlock()
return s.store(s.fullPath(passwordPrefix, id), passwordFile, password)
}

//===========================================================================
// Certificate Methods
//===========================================================================

// GetCertificate retrieves a certificate by id from the local storage backend.
func (s *Store) GetCertificate(name string) (cert []byte, err error) {
s.RLock()
defer s.RUnlock()
return s.load(s.fullPath(certificatePrefix, name))
}

// UpdateCertificate updates a certificate in the local storage backend.
func (s *Store) UpdateCertificate(name string, cert []byte) (err error) {
s.Lock()
defer s.Unlock()
return s.store(s.fullPath(certificatePrefix, name), certificateFile, cert)
}

//===========================================================================
// Helper methods
//===========================================================================

// fullPath returns the full path to an archive file in the local storage backend.
func (s *Store) fullPath(prefix, name string) string {
return filepath.Join(s.path, prefix+"-"+name+".zip")
}

// load returns file data by archive path from the local storage
func (s *Store) load(path string) (data []byte, err error) {
var archive *zip.ReadCloser
if archive, err = zip.OpenReader(path); err != nil {
if os.IsNotExist(err) {
return nil, store.ErrNotFound
}
return nil, err
}
defer archive.Close()

// Load the file from the archive
if len(archive.File) == 0 {
return nil, store.ErrNotFound
}

var reader io.ReadCloser
if reader, err = archive.File[0].Open(); err != nil {
return nil, err
}
defer reader.Close()

return io.ReadAll(reader)
}

// store saves file data to an archive and file name in the local storage
func (s *Store) store(path, name string, data []byte) (err error) {
var archive *os.File
if archive, err = os.Create(path); err != nil {
return err
}
defer archive.Close()

// Write the file to the archive
zipWriter := zip.NewWriter(archive)
defer zipWriter.Close()

var writer io.Writer
if writer, err = zipWriter.Create(name); err != nil {
return err
}

if _, err = writer.Write(data); err != nil {
return err
}

return nil
}
74 changes: 74 additions & 0 deletions pkg/store/local/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package local_test

import (
"os"
"testing"

"github.com/stretchr/testify/suite"
"github.com/trisacrypto/courier/pkg/config"
"github.com/trisacrypto/courier/pkg/store"
"github.com/trisacrypto/courier/pkg/store/local"
)

type localStoreTestSuite struct {
suite.Suite
conf config.LocalStorageConfig
store *local.Store
}

func (s *localStoreTestSuite) SetupSuite() {
// Open the storage backend in a temporary directory
var err error
path := s.T().TempDir()
s.store, err = local.Open(config.LocalStorageConfig{
Enabled: true,
Path: path,
})
s.NoError(err, "could not open local storage backend")
}

func (s *localStoreTestSuite) TearDownSuite() {
// Remove the temporary directory
s.NoError(s.store.Close(), "could not close local storage backend")
s.NoError(os.RemoveAll(s.conf.Path), "could not remove temporary directory")
}

func TestLocalStore(t *testing.T) {
suite.Run(t, new(localStoreTestSuite))
}

func (s *localStoreTestSuite) TestPasswordStore() {
require := s.Require()

// Try to get a password that does not exist
_, err := s.store.GetPassword("does-not-exist")
require.ErrorIs(err, store.ErrNotFound, "should return error if password does not exist")

// Create a password
password := []byte("password")
err = s.store.UpdatePassword("password_id", password)
require.NoError(err, "should be able to create a password")

// Get the password
actual, err := s.store.GetPassword("password_id")
require.NoError(err, "should be able to get a password")
require.Equal(password, actual, "wrong password returned")
}

func (s *localStoreTestSuite) TestCertificateStore() {
require := s.Require()

// Try to get a certificate that does not exist
_, err := s.store.GetCertificate("does-not-exist")
require.ErrorIs(err, store.ErrNotFound, "should return error if certificate does not exist")

// Create a certificate
cert := []byte("certificate")
err = s.store.UpdateCertificate("certificate_id", cert)
require.NoError(err, "should be able to create a certificate")

// Get the certificate
actual, err := s.store.GetCertificate("certificate_id")
require.NoError(err, "should be able to get a certificate")
require.Equal(cert, actual, "wrong certificate returned")
}
20 changes: 20 additions & 0 deletions pkg/store/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package store

// Store is a generic interface for storing and retrieving data.
type Store interface {
Close() error
PasswordStore
CertificateStore
}

// PasswordStore is a generic interface for storing and retrieving passwords.
type PasswordStore interface {
GetPassword(name string) ([]byte, error)
UpdatePassword(name string, password []byte) error
}

// CertificateStore is a generic interface for storing and retrieving certificates.
type CertificateStore interface {
GetCertificate(name string) ([]byte, error)
UpdateCertificate(name string, cert []byte) error
}

0 comments on commit 9e6e020

Please sign in to comment.