Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds a new raw file metadata storage for clients #347

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
dc9d46c
Added first draft for a raw json local file storage.
kommendorkapten Jul 28, 2022
c41bd2d
Ignore emacs temporary files
kommendorkapten Jul 28, 2022
9fdf332
moved isMetaFile to util directory for reuse in other packages.
kommendorkapten Jul 29, 2022
1acc7bf
Added unit tests and refactored code to match the local store for rep…
kommendorkapten Jul 29, 2022
c527e56
Added test case for non json file in metadata directory.
kommendorkapten Jul 29, 2022
b7dbd08
Use os.MkdirAll when creating metadata cache.
kommendorkapten Jul 29, 2022
2dc5d0e
More consistent naming, added comments and a unit test.
kommendorkapten Aug 1, 2022
ad4974b
Added a localstore wrapper for concurrent access.
kommendorkapten Aug 4, 2022
61c7323
Update client/filejsonstore/filejsonstore_test.go
kommendorkapten Aug 3, 2022
4714ff1
Added tests to make sure returned struct implements LocalStore interf…
kommendorkapten Aug 4, 2022
4048848
Update client/filejsonstore/filejsonstore.go
kommendorkapten Aug 9, 2022
1d074f8
Update client/filejsonstore/filejsonstore_test.go
kommendorkapten Aug 9, 2022
18f7ce7
Update client/filejsonstore/filejsonstore.go
kommendorkapten Aug 10, 2022
800da60
Update client/filejsonstore/filejsonstore_test.go
kommendorkapten Aug 10, 2022
0627a27
Made FileJSONStore safe for concurrent access.
kommendorkapten Aug 10, 2022
83c0eab
Spelling error.
kommendorkapten Aug 11, 2022
0cbf332
Updates based on PR comments.
kommendorkapten Aug 12, 2022
7a935d6
Added first draft for a raw json local file storage.
kommendorkapten Jul 28, 2022
c0cc5fc
Ignore emacs temporary files
kommendorkapten Jul 28, 2022
ad8f340
moved isMetaFile to util directory for reuse in other packages.
kommendorkapten Jul 29, 2022
d1956d6
Added unit tests and refactored code to match the local store for rep…
kommendorkapten Jul 29, 2022
678b7f9
Added test case for non json file in metadata directory.
kommendorkapten Jul 29, 2022
207fb70
Use os.MkdirAll when creating metadata cache.
kommendorkapten Jul 29, 2022
7916ddf
More consistent naming, added comments and a unit test.
kommendorkapten Aug 1, 2022
ae6b3d8
Added a localstore wrapper for concurrent access.
kommendorkapten Aug 4, 2022
3a1cce0
Update client/filejsonstore/filejsonstore_test.go
kommendorkapten Aug 3, 2022
a42ffd5
Added tests to make sure returned struct implements LocalStore interf…
kommendorkapten Aug 4, 2022
0e6634c
Update client/filejsonstore/filejsonstore.go
kommendorkapten Aug 9, 2022
9c5b024
Update client/filejsonstore/filejsonstore_test.go
kommendorkapten Aug 9, 2022
0257d6d
Update client/filejsonstore/filejsonstore.go
kommendorkapten Aug 10, 2022
dafc161
Update client/filejsonstore/filejsonstore_test.go
kommendorkapten Aug 10, 2022
96769ff
Made FileJSONStore safe for concurrent access.
kommendorkapten Aug 10, 2022
86d4b33
Spelling error.
kommendorkapten Aug 11, 2022
c33254e
Updates based on PR comments.
kommendorkapten Aug 12, 2022
f95757c
Disabled filesystem permission checks for windows.
kommendorkapten Aug 19, 2022
98f76b0
Merge branch 'md_raw_json' of github.com:kommendorkapten/go-tuf into …
kommendorkapten Aug 19, 2022
7301a8c
Moved the tests that rely on permission bits to a new test file.
kommendorkapten Aug 22, 2022
5dd9632
Merge branch 'master' into md_raw_json
kommendorkapten Aug 23, 2022
39f4018
Clarify permissions check naming and comment, add tests
ethan-lowman-dd Aug 23, 2022
01bbd76
Update internal/fsutil/fsutil.go
kommendorkapten Aug 24, 2022
19ef1ac
Updates based on feeback.
kommendorkapten Aug 24, 2022
db6ad66
Use fs.ErrNotExist instead of os.ErrNotExist as recommended by Go docs
ethan-lowman-dd Aug 24, 2022
ff675c6
Clean up remaining unnecessary usage of filepath.FromSlash
ethan-lowman-dd Aug 24, 2022
9ca46f4
Add missing error checks in tests
ethan-lowman-dd Aug 24, 2022
86e5085
Merge branch 'master' into md_raw_json
kommendorkapten Sep 12, 2022
94cf072
Merge branch 'master' into md_raw_json
kommendorkapten Sep 12, 2022
210b417
Make sure to test that returned err is nil in the tests.
kommendorkapten Sep 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
cmd/tuf/tuf
cmd/tuf-client/tuf-client
.vscode
*~
148 changes: 148 additions & 0 deletions client/filejsonstore/filejsonstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package client

import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"

"github.com/theupdateframework/go-tuf/client"
"github.com/theupdateframework/go-tuf/internal/fsutil"
"github.com/theupdateframework/go-tuf/util"
)

const (
// user: rwx
// group: r-x
// other: ---
dirCreateMode = os.FileMode(0750)
// user: rw-
// group: r--
// other: ---
fileCreateMode = os.FileMode(0640)
)

// FileJSONStore represents a local metadata cache relying on raw JSON files
// as retrieved from the remote repository.
type FileJSONStore struct {
mtx sync.RWMutex
baseDir string
}

var _ client.LocalStore = (*FileJSONStore)(nil)

// NewFileJSONStore returns a new metadata cache, implemented using raw JSON
// files, stored in a directory provided by the client.
// If the provided directory does not exist on disk, it will be created.
// The provided metadata cache is safe for concurrent access.
func NewFileJSONStore(baseDir string) (*FileJSONStore, error) {
f := &FileJSONStore{
baseDir: baseDir,
}

// Does the directory exist?
fi, err := os.Stat(baseDir)
kommendorkapten marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// Create the directory
if err = os.MkdirAll(baseDir, dirCreateMode); err != nil {
return nil, fmt.Errorf("error creating directory for metadata cache: %w", err)
}
} else {
return nil, fmt.Errorf("error getting FileInfo for %s: %w", baseDir, err)
}
} else {
// Verify that it is a directory
if !fi.IsDir() {
return nil, fmt.Errorf("can not open %s, not a directory", baseDir)
}
// Verify file mode is not too permissive.
if err = fsutil.EnsureMaxPermissions(fi, dirCreateMode); err != nil {
return nil, err
}
}

return f, nil
}

// GetMeta returns the currently cached set of metadata files.
func (f *FileJSONStore) GetMeta() (map[string]json.RawMessage, error) {
f.mtx.RLock()
defer f.mtx.RUnlock()

kommendorkapten marked this conversation as resolved.
Show resolved Hide resolved
names, err := os.ReadDir(f.baseDir)
if err != nil {
return nil, fmt.Errorf("error reading directory %s: %w", f.baseDir, err)
}

meta := map[string]json.RawMessage{}
for _, name := range names {
ok, err := fsutil.IsMetaFile(name)
if err != nil {
return nil, err
}
if !ok {
continue
}

// Verify permissions
info, err := name.Info()
if err != nil {
return nil, fmt.Errorf("error retrieving FileInfo for %s: %w", name.Name(), err)
}
if err = fsutil.EnsureMaxPermissions(info, fileCreateMode); err != nil {
return nil, err
}

p := filepath.Join(f.baseDir, name.Name())
b, err := os.ReadFile(p)
if err != nil {
return nil, fmt.Errorf("error reading file %s: %w", name.Name(), err)
}
meta[name.Name()] = b
}

return meta, nil
}

// SetMeta stores a metadata file in the cache. If the metadata file exist,
// it will be overwritten.
func (f *FileJSONStore) SetMeta(name string, meta json.RawMessage) error {
f.mtx.Lock()
defer f.mtx.Unlock()

if filepath.Ext(name) != ".json" {
return fmt.Errorf("file %s is not a JSON file", name)
}

p := filepath.Join(f.baseDir, name)
err := util.AtomicallyWriteFile(p, meta, fileCreateMode)
return err
}

// DeleteMeta deletes a metadata file from the cache.
// If the file does not exist, an *os.PathError is returned.
func (f *FileJSONStore) DeleteMeta(name string) error {
f.mtx.Lock()
defer f.mtx.Unlock()

if filepath.Ext(name) != ".json" {
return fmt.Errorf("file %s is not a JSON file", name)
}

p := filepath.Join(f.baseDir, name)
err := os.Remove(p)
if err == nil {
return nil
}

return fmt.Errorf("error deleting file %s: %w", name, err)
}

// Close closes the metadata cache. This is a no-op.
func (f *FileJSONStore) Close() error {
return nil
}
194 changes: 194 additions & 0 deletions client/filejsonstore/filejsonstore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package client

import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"

"gopkg.in/check.v1"
)

type FileJSONStoreSuite struct{}

var _ = check.Suite(&FileJSONStoreSuite{})

// Hook up gocheck into the "go test" runner
func Test(t *testing.T) { check.TestingT(t) }

func (FileJSONStoreSuite) TestNewOk(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")

// Assert path does not exist
fi, err := os.Stat(p)
c.Assert(fi, check.IsNil)
c.Assert(errors.Is(err, os.ErrNotExist), check.Equals, true)

// Create implementation
s, err := NewFileJSONStore(p)
c.Assert(err, check.IsNil)
c.Assert(s, check.NotNil)

// Assert path does exist and is a directory
fi, err = os.Stat(p)
c.Assert(fi, check.NotNil)
c.Assert(err, check.IsNil)
c.Assert(fi.IsDir(), check.Equals, true)
}

func (FileJSONStoreSuite) TestNewFileExists(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")

// Create an empty file
f, err := os.Create(p)
c.Assert(err, check.IsNil)
f.Close()

// Create implementation
s, err := NewFileJSONStore(p)
c.Assert(s, check.IsNil)
c.Assert(err, check.NotNil)
found := strings.Contains(err.Error(), ", not a directory")
c.Assert(found, check.Equals, true)
}

func (FileJSONStoreSuite) TestNewDirectoryExists(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")

err := os.Mkdir(p, 0750)
c.Assert(err, check.IsNil)

// Create implementation
s, err := NewFileJSONStore(p)
c.Assert(s, check.NotNil)
c.Assert(err, check.IsNil)
}

func (FileJSONStoreSuite) TestGetMetaEmpty(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")
s, err := NewFileJSONStore(p)
c.Assert(err, check.IsNil)

md, err := s.GetMeta()
c.Assert(err, check.IsNil)
c.Assert(md, check.HasLen, 0)
}

func (FileJSONStoreSuite) TestGetNoDirectory(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")
s, err := NewFileJSONStore(p)
c.Assert(err, check.IsNil)

err = os.Remove(p)
c.Assert(err, check.IsNil)

md, err := s.GetMeta()
c.Assert(md, check.IsNil)
c.Assert(err, check.NotNil)
}

func (FileJSONStoreSuite) TestMetadataOperations(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")
s, err := NewFileJSONStore(p)
c.Assert(err, check.IsNil)

expected := map[string]json.RawMessage{
"file1.json": []byte{0xf1, 0xe1, 0xd1},
"file2.json": []byte{0xf2, 0xe2, 0xd2},
"file3.json": []byte{0xf3, 0xe3, 0xd3},
}

for k, v := range expected {
err := s.SetMeta(k, v)
c.Assert(err, check.IsNil)
}

md, err := s.GetMeta()
c.Assert(err, check.IsNil)
c.Assert(md, check.HasLen, 3)
c.Assert(md, check.DeepEquals, expected)

// Delete all items
count := 3
for k := range expected {
err = s.DeleteMeta(k)
c.Assert(err, check.IsNil)

md, err := s.GetMeta()
c.Assert(err, check.IsNil)

count--
c.Assert(md, check.HasLen, count)
}

md, err = s.GetMeta()
c.Assert(err, check.IsNil)
c.Assert(md, check.HasLen, 0)
}

func (FileJSONStoreSuite) TestGetNoJSON(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")
s, err := NewFileJSONStore(p)
c.Assert(s, check.NotNil)
c.Assert(err, check.IsNil)

// Create a file which does not end with '.json'
fp := filepath.Join(p, "meta.xml")
err = os.WriteFile(fp, []byte{}, 0644)
c.Assert(err, check.IsNil)

md, err := s.GetMeta()
c.Assert(err, check.IsNil)
c.Assert(md, check.HasLen, 0)
}

func (FileJSONStoreSuite) TestNoJSON(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")
s, err := NewFileJSONStore(p)
c.Assert(s, check.NotNil)
c.Assert(err, check.IsNil)

files := []string{
"file.xml",
"file",
"",
}
for _, f := range files {
err := s.SetMeta(f, []byte{})
c.Assert(err, check.ErrorMatches, "file.*is not a JSON file")
}
}

func (FileJSONStoreSuite) TestClose(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")
s, err := NewFileJSONStore(p)
c.Assert(s, check.NotNil)
c.Assert(err, check.IsNil)

err = s.Close()
c.Assert(err, check.IsNil)
}

func (FileJSONStoreSuite) TestDelete(c *check.C) {
tmp := c.MkDir()
p := filepath.Join(tmp, "tuf_raw.db")
s, err := NewFileJSONStore(p)
c.Assert(s, check.NotNil)
c.Assert(err, check.IsNil)

err = s.DeleteMeta("not_json.yml")
c.Assert(err, check.ErrorMatches, "file not_json\\.yml is not a JSON file")
err = s.DeleteMeta("non_existing.json")
c.Assert(errors.Is(err, os.ErrNotExist), check.Equals, true)
}
Loading