Skip to content

Commit

Permalink
Implement MakeDirectoryArtifactsProcessor (#4)
Browse files Browse the repository at this point in the history
* Implement MakeDirectoryArtifactsProcessor
  • Loading branch information
jwillp authored Aug 22, 2024
1 parent 782d328 commit 78728ec
Show file tree
Hide file tree
Showing 8 changed files with 532 additions and 317 deletions.
59 changes: 58 additions & 1 deletion artefactregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,71 @@ import (
"time"
)

var _ ArtifactRegistry = (*InMemoryArtifactRegistry)(nil)

type InMemoryArtifactRegistry struct {
ArtifactMap map[string][]string
mu sync.RWMutex // Mutex to protect concurrent access
}

func (r *InMemoryArtifactRegistry) Load() error { return nil }

func (r *InMemoryArtifactRegistry) Save() error { return nil }

func (r *InMemoryArtifactRegistry) AddArtifact(processorName string, artifactName string) {
r.mu.Lock()
defer r.mu.Unlock()
if r.ArtifactMap == nil {
r.ArtifactMap = map[string][]string{}
}

if _, ok := r.ArtifactMap[processorName]; !ok {
r.ArtifactMap[processorName] = make([]string, 0)
}
r.ArtifactMap[processorName] = append(r.ArtifactMap[processorName], artifactName)
}

func (r *InMemoryArtifactRegistry) RemoveArtifact(processorName string, artifactName string) {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.ArtifactMap[processorName]; !ok {
return
}
if r.ArtifactMap == nil {
r.ArtifactMap = map[string][]string{}
}

var artifacts []string
for _, file := range r.ArtifactMap[processorName] {
if file != artifactName {
artifacts = append(artifacts, file)
}
}

r.ArtifactMap[processorName] = artifacts
}

func (r *InMemoryArtifactRegistry) Artifacts(processorName string) []string {
if r.ArtifactMap == nil {
r.ArtifactMap = map[string][]string{}
}

values, ok := r.ArtifactMap[processorName]
if !ok {
return nil
}

return values
}

// JSONArtifactRegistry implementation of a ArtifactRegistry that is saved as a JSON file.
type JSONArtifactRegistry struct {
GeneratedAt time.Time `json:"generatedAt"`
ArtifactMap map[string]*JSONArtifactRegistryProcessor `json:"files"`
FilePath string `json:"-"`
FileSystem FileSystem `json:"-"`
mu sync.RWMutex // Mutex to protect concurrent access
CurrentTimeProvider func() time.Time
CurrentTimeProvider func() time.Time `json:"-"`
}

type JSONArtifactRegistryProcessor struct {
Expand Down
83 changes: 83 additions & 0 deletions filesystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package specter

import (
"io/fs"
"os"
"path/filepath"
)

// FileSystem is an abstraction layer over various types of file systems.
// It provides a unified interface to interact with different file system implementations,
// whether they are local, remote, or virtual.
//
// Implementations of this interface allow consumers to perform common file system operations such as
// obtaining absolute paths, retrieving file information, walking through directories, and reading files.
type FileSystem interface {
// Abs converts a relative file path to an absolute one. The implementation may
// differ depending on the underlying file system.
Abs(location string) (string, error)

// StatPath returns file information for the specified location. This typically
// includes details like size, modification time, and whether the path is a file
// or directory.
StatPath(location string) (os.FileInfo, error)

// WalkDir traverses the directory tree rooted at the specified path, calling the
// provided function for each file or directory encountered. This allows for
// efficient processing of large directory structures and can handle errors for
// individual files or directories.
WalkDir(dirPath string, f func(path string, d fs.DirEntry, err error) error) error

// ReadFile reads the contents of the specified file and returns it as a byte
// slice. This method abstracts away the specifics of how the file is accessed,
// making it easier to work with files across different types of file systems.
ReadFile(filePath string) ([]byte, error)

// WriteFile writes data to the specified file path with the given permissions.
// If the file exists, it will be overwritten.
WriteFile(path string, data []byte, perm fs.FileMode) error

// Mkdir creates a new directory at the specified path along with any necessary
// parents, and returns nil, If the directory already exists, the implementation
// may either return an error or ignore the request, depending on the file
// system. This method abstracts the underlying mechanism of directory creation
// across different file systems.
Mkdir(dirPath string, mode fs.FileMode) error

// Remove removes the named file or (empty) directory.
// If there is an error, it will be of type *PathError.
Remove(path string) error
}

var _ FileSystem = LocalFileSystem{}

// LocalFileSystem is an implementation of a FileSystem that works on the local file system where this program is running.
type LocalFileSystem struct{}

func (l LocalFileSystem) Remove(path string) error {
return os.Remove(path)
}

func (l LocalFileSystem) WriteFile(path string, data []byte, perm fs.FileMode) error {
return os.WriteFile(path, data, perm)
}

func (l LocalFileSystem) Mkdir(dirPath string, mode fs.FileMode) error {
return os.MkdirAll(dirPath, mode)
}

func (l LocalFileSystem) ReadFile(filePath string) ([]byte, error) {
return os.ReadFile(filePath)
}

func (l LocalFileSystem) WalkDir(dirPath string, f func(path string, d fs.DirEntry, err error) error) error {
return filepath.WalkDir(dirPath, f)
}

func (l LocalFileSystem) StatPath(location string) (os.FileInfo, error) {
return os.Stat(location)
}

func (l LocalFileSystem) Abs(location string) (string, error) {
return filepath.Abs(location)
}
213 changes: 213 additions & 0 deletions filesystem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package specter

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/fs"
"os"
"runtime"
"strings"
"testing"
)

var _ FileSystem = (*mockFileSystem)(nil)

// Mock implementations to use in tests.
type mockFileInfo struct {
os.FileInfo
name string
size int64
mode os.FileMode
modTime int64
isDir bool
}

func (m mockFileInfo) Type() fs.FileMode {
if m.isDir {
return os.ModeDir
}

return os.ModeAppend
}

func (m mockFileInfo) Info() (fs.FileInfo, error) {
return m, nil
}

func (m mockFileInfo) Name() string { return m.name }
func (m mockFileInfo) Size() int64 { return m.size }
func (m mockFileInfo) Mode() os.FileMode { return m.mode }
func (m mockFileInfo) IsDir() bool { return m.isDir }
func (m mockFileInfo) Sys() interface{} { return nil }

type mockFileSystem struct {
files map[string][]byte
dirs map[string]bool

absErr error
statErr error
walkDirErr error
readFileErr error
writeFileErr error
mkdirErr error
rmErr error
}

func (m *mockFileSystem) WriteFile(filePath string, data []byte, _ fs.FileMode) error {
if m.writeFileErr != nil {
return m.writeFileErr
}

if m.files == nil {
m.files = map[string][]byte{}
}

m.files[filePath] = data

return nil
}

func (m *mockFileSystem) Mkdir(dirPath string, _ fs.FileMode) error {
if m.mkdirErr != nil {
return m.mkdirErr
}

m.dirs[dirPath] = true

return nil
}

func (m *mockFileSystem) Remove(path string) error {
if m.rmErr != nil {
return m.rmErr
}

m.dirs[path] = false
delete(m.files, path)

return nil
}

func (m *mockFileSystem) Abs(location string) (string, error) {
if m.absErr != nil {
return "", m.absErr
}

absPaths := make(map[string]bool, len(m.files)+len(m.dirs))

for k := range m.files {
absPaths[k] = true
}
for k := range m.dirs {
absPaths[k] = true
}

if absPath, exists := absPaths[location]; exists && absPath {
return location, nil
}
return "", nil
}

func (m *mockFileSystem) StatPath(location string) (os.FileInfo, error) {
if m.statErr != nil {
return nil, m.statErr
}

if isDir, exists := m.dirs[location]; exists {
return mockFileInfo{name: location, isDir: isDir}, nil
}

if data, exists := m.files[location]; exists {
return mockFileInfo{name: location, size: int64(len(data))}, nil
}

return nil, os.ErrNotExist
}

func (m *mockFileSystem) WalkDir(dirPath string, f func(path string, d fs.DirEntry, err error) error) error {
if m.walkDirErr != nil {
return m.walkDirErr
}

for path, isDir := range m.dirs {
if strings.HasPrefix(path, dirPath) {
err := f(path, mockFileInfo{name: path, isDir: isDir}, nil)
if err != nil {
return err
}
}
}

for path := range m.files {
if strings.HasPrefix(path, dirPath) {
err := f(path, mockFileInfo{name: path, isDir: false}, nil)
if err != nil {
return err
}
}
}

return nil
}

func (m *mockFileSystem) ReadFile(filePath string) ([]byte, error) {
if m.readFileErr != nil {
return nil, m.readFileErr
}

if data, exists := m.files[filePath]; exists {
return data, nil
}

return nil, os.ErrNotExist
}

func TestLocalFileSystem_ReadFile(t *testing.T) {
tests := []struct {
name string
given string
then []byte
thenErrContains string
}{
{
name: "GIVEN a file that does not exists THEN return error",
given: "/fake/dir/file1.txt",
thenErrContains: "no such file or directory",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := LocalFileSystem{}
got, err := l.ReadFile(tt.given)

if tt.thenErrContains != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.thenErrContains)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.then, got)
})
}
}

func TestLocalFileSystem_WalkDir(t *testing.T) {
// Make sure the closure gets called.
lfs := LocalFileSystem{}
closureCalled := false
err := lfs.WalkDir("/fake/dir", func(path string, d fs.DirEntry, err error) error {
closureCalled = true
return nil
})
require.True(t, closureCalled)
require.NoError(t, err)
}

func TestLocalFileSystem_StatPath(t *testing.T) {
lfs := LocalFileSystem{}
_, filename, _, ok := runtime.Caller(0)
require.True(t, ok)
stat, err := lfs.StatPath(filename)
require.NoError(t, err)
assert.NotNil(t, stat)
}
Loading

0 comments on commit 78728ec

Please sign in to comment.