From 25b12a22a447a32b69bea342e02471fcc674dc29 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 22:00:45 +0000 Subject: [PATCH] refactor(ax): add memory medium aliases --- bench_test.go | 24 ++++++------ client_test.go | 16 ++++---- datanode/client.go | 90 ++++++++++++++++++++------------------------ io.go | 22 ++++++++++- node/node.go | 2 +- node/node_test.go | 6 +-- workspace/service.go | 20 +++++----- 7 files changed, 93 insertions(+), 87 deletions(-) diff --git a/bench_test.go b/bench_test.go index df24267..dd259d0 100644 --- a/bench_test.go +++ b/bench_test.go @@ -4,31 +4,31 @@ import ( "testing" ) -func BenchmarkMockMedium_Write(b *testing.B) { - m := NewMockMedium() +func BenchmarkMemoryMedium_Write(b *testing.B) { + medium := NewMemoryMedium() b.ResetTimer() for i := 0; i < b.N; i++ { - _ = m.Write("test.txt", "some content") + _ = medium.Write("test.txt", "some content") } } -func BenchmarkMockMedium_Read(b *testing.B) { - m := NewMockMedium() - _ = m.Write("test.txt", "some content") +func BenchmarkMemoryMedium_Read(b *testing.B) { + medium := NewMemoryMedium() + _ = medium.Write("test.txt", "some content") b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = m.Read("test.txt") + _, _ = medium.Read("test.txt") } } -func BenchmarkMockMedium_List(b *testing.B) { - m := NewMockMedium() - _ = m.EnsureDir("dir") +func BenchmarkMemoryMedium_List(b *testing.B) { + medium := NewMemoryMedium() + _ = medium.EnsureDir("dir") for i := 0; i < 100; i++ { - _ = m.Write("dir/file"+string(rune(i))+".txt", "content") + _ = medium.Write("dir/file"+string(rune(i))+".txt", "content") } b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = m.List("dir") + _, _ = medium.List("dir") } } diff --git a/client_test.go b/client_test.go index d59219e..f6e3e26 100644 --- a/client_test.go +++ b/client_test.go @@ -9,15 +9,15 @@ import ( "github.com/stretchr/testify/require" ) -// --- MockMedium Tests --- +// --- MemoryMedium Compatibility Tests --- -func TestClient_NewMockMedium_Good(t *testing.T) { - m := NewMockMedium() - assert.NotNil(t, m) - assert.NotNil(t, m.Files) - assert.NotNil(t, m.Dirs) - assert.Empty(t, m.Files) - assert.Empty(t, m.Dirs) +func TestClient_NewMemoryMedium_Good(t *testing.T) { + medium := NewMemoryMedium() + assert.NotNil(t, medium) + assert.NotNil(t, medium.Files) + assert.NotNil(t, medium.Dirs) + assert.Empty(t, medium.Files) + assert.Empty(t, medium.Dirs) } func TestClient_MockMedium_Read_Good(t *testing.T) { diff --git a/datanode/client.go b/datanode/client.go index d551ec5..05e11e6 100644 --- a/datanode/client.go +++ b/datanode/client.go @@ -37,7 +37,7 @@ var ( type Medium struct { dataNode *borgdatanode.DataNode directorySet map[string]bool // explicit directories that exist without file contents - mu sync.RWMutex + lock sync.RWMutex } func New() *Medium { @@ -63,8 +63,8 @@ func FromTar(data []byte) (*Medium, error) { // Example: snapshot, _ := medium.Snapshot() func (medium *Medium) Snapshot() ([]byte, error) { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() data, err := medium.dataNode.ToTar() if err != nil { return nil, core.E("datanode.Snapshot", "tar failed", err) @@ -78,8 +78,8 @@ func (medium *Medium) Restore(data []byte) error { if err != nil { return core.E("datanode.Restore", "tar failed", err) } - medium.mu.Lock() - defer medium.mu.Unlock() + medium.lock.Lock() + defer medium.lock.Unlock() medium.dataNode = dataNode medium.directorySet = make(map[string]bool) return nil @@ -87,8 +87,8 @@ func (medium *Medium) Restore(data []byte) error { // Example: dataNode := medium.DataNode() func (medium *Medium) DataNode() *borgdatanode.DataNode { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() return medium.dataNode } @@ -105,8 +105,8 @@ func normaliseEntryPath(filePath string) string { // --- io.Medium interface --- func (medium *Medium) Read(filePath string) (string, error) { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() filePath = normaliseEntryPath(filePath) file, err := medium.dataNode.Open(filePath) @@ -131,8 +131,8 @@ func (medium *Medium) Read(filePath string) (string, error) { } func (medium *Medium) Write(filePath, content string) error { - medium.mu.Lock() - defer medium.mu.Unlock() + medium.lock.Lock() + defer medium.lock.Unlock() filePath = normaliseEntryPath(filePath) if filePath == "" { @@ -150,8 +150,8 @@ func (medium *Medium) WriteMode(filePath, content string, mode fs.FileMode) erro } func (medium *Medium) EnsureDir(filePath string) error { - medium.mu.Lock() - defer medium.mu.Unlock() + medium.lock.Lock() + defer medium.lock.Unlock() filePath = normaliseEntryPath(filePath) if filePath == "" { @@ -162,7 +162,7 @@ func (medium *Medium) EnsureDir(filePath string) error { } // ensureDirsLocked marks a directory and all ancestors as existing. -// Caller must hold medium.mu. +// Caller must hold medium.lock. func (medium *Medium) ensureDirsLocked(directoryPath string) { for directoryPath != "" && directoryPath != "." { medium.directorySet[directoryPath] = true @@ -174,8 +174,8 @@ func (medium *Medium) ensureDirsLocked(directoryPath string) { } func (medium *Medium) IsFile(filePath string) bool { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() filePath = normaliseEntryPath(filePath) info, err := medium.dataNode.Stat(filePath) @@ -191,20 +191,17 @@ func (medium *Medium) FileSet(filePath, content string) error { } func (medium *Medium) Delete(filePath string) error { - medium.mu.Lock() - defer medium.mu.Unlock() + medium.lock.Lock() + defer medium.lock.Unlock() filePath = normaliseEntryPath(filePath) if filePath == "" { return core.E("datanode.Delete", "cannot delete root", fs.ErrPermission) } - // Check if it's a file in the DataNode info, err := medium.dataNode.Stat(filePath) if err != nil { - // Check explicit directories if medium.directorySet[filePath] { - // Check if dir is empty hasChildren, err := medium.hasPrefixLocked(filePath + "/") if err != nil { return core.E("datanode.Delete", core.Concat("failed to inspect directory: ", filePath), err) @@ -238,8 +235,8 @@ func (medium *Medium) Delete(filePath string) error { } func (medium *Medium) DeleteAll(filePath string) error { - medium.mu.Lock() - defer medium.mu.Unlock() + medium.lock.Lock() + defer medium.lock.Unlock() filePath = normaliseEntryPath(filePath) if filePath == "" { @@ -249,7 +246,6 @@ func (medium *Medium) DeleteAll(filePath string) error { prefix := filePath + "/" found := false - // Check if filePath itself is a file info, err := medium.dataNode.Stat(filePath) if err == nil && !info.IsDir() { if err := medium.removeFileLocked(filePath); err != nil { @@ -287,20 +283,18 @@ func (medium *Medium) DeleteAll(filePath string) error { } func (medium *Medium) Rename(oldPath, newPath string) error { - medium.mu.Lock() - defer medium.mu.Unlock() + medium.lock.Lock() + defer medium.lock.Unlock() oldPath = normaliseEntryPath(oldPath) newPath = normaliseEntryPath(newPath) - // Check if source is a file info, err := medium.dataNode.Stat(oldPath) if err != nil { return core.E("datanode.Rename", core.Concat("not found: ", oldPath), fs.ErrNotExist) } if !info.IsDir() { - // Read old, write new, delete old data, err := medium.readFileLocked(oldPath) if err != nil { return core.E("datanode.Rename", core.Concat("failed to read source file: ", oldPath), err) @@ -313,7 +307,6 @@ func (medium *Medium) Rename(oldPath, newPath string) error { return nil } - // Directory rename: move all files under oldPath to newPath oldPrefix := oldPath + "/" newPrefix := newPath + "/" @@ -335,7 +328,6 @@ func (medium *Medium) Rename(oldPath, newPath string) error { } } - // Move explicit directories dirsToMove := make(map[string]string) for directoryPath := range medium.directorySet { if directoryPath == oldPath || core.HasPrefix(directoryPath, oldPrefix) { @@ -352,14 +344,13 @@ func (medium *Medium) Rename(oldPath, newPath string) error { } func (medium *Medium) List(filePath string) ([]fs.DirEntry, error) { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() filePath = normaliseEntryPath(filePath) entries, err := medium.dataNode.ReadDir(filePath) if err != nil { - // Check explicit directories if filePath == "" || medium.directorySet[filePath] { return []fs.DirEntry{}, nil } @@ -399,8 +390,8 @@ func (medium *Medium) List(filePath string) ([]fs.DirEntry, error) { } func (medium *Medium) Stat(filePath string) (fs.FileInfo, error) { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() filePath = normaliseEntryPath(filePath) if filePath == "" { @@ -419,8 +410,8 @@ func (medium *Medium) Stat(filePath string) (fs.FileInfo, error) { } func (medium *Medium) Open(filePath string) (fs.File, error) { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() filePath = normaliseEntryPath(filePath) return medium.dataNode.Open(filePath) @@ -440,25 +431,24 @@ func (medium *Medium) Append(filePath string) (goio.WriteCloser, error) { return nil, core.E("datanode.Append", "empty path", fs.ErrInvalid) } - // Read existing content var existing []byte - medium.mu.RLock() + medium.lock.RLock() if medium.IsFile(filePath) { data, err := medium.readFileLocked(filePath) if err != nil { - medium.mu.RUnlock() + medium.lock.RUnlock() return nil, core.E("datanode.Append", core.Concat("failed to read existing content: ", filePath), err) } existing = data } - medium.mu.RUnlock() + medium.lock.RUnlock() return &writeCloser{medium: medium, path: filePath, buf: existing}, nil } func (medium *Medium) ReadStream(filePath string) (goio.ReadCloser, error) { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() filePath = normaliseEntryPath(filePath) file, err := medium.dataNode.Open(filePath) @@ -473,8 +463,8 @@ func (medium *Medium) WriteStream(filePath string) (goio.WriteCloser, error) { } func (medium *Medium) Exists(filePath string) bool { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() filePath = normaliseEntryPath(filePath) if filePath == "" { @@ -488,8 +478,8 @@ func (medium *Medium) Exists(filePath string) bool { } func (medium *Medium) IsDir(filePath string) bool { - medium.mu.RLock() - defer medium.mu.RUnlock() + medium.lock.RLock() + defer medium.lock.RUnlock() filePath = normaliseEntryPath(filePath) if filePath == "" { @@ -556,7 +546,7 @@ func (medium *Medium) readFileLocked(filePath string) ([]byte, error) { // removeFileLocked removes a single file by rebuilding the DataNode. // This is necessary because Borg's DataNode doesn't expose a Remove method. -// Caller must hold medium.mu write lock. +// Caller must hold medium.lock write lock. func (medium *Medium) removeFileLocked(target string) error { entries, err := medium.collectAllLocked() if err != nil { @@ -591,8 +581,8 @@ func (writer *writeCloser) Write(data []byte) (int, error) { } func (writer *writeCloser) Close() error { - writer.medium.mu.Lock() - defer writer.medium.mu.Unlock() + writer.medium.lock.Lock() + defer writer.medium.lock.Unlock() writer.medium.dataNode.AddData(writer.path, writer.buf) writer.medium.ensureDirsLocked(path.Dir(writer.path)) diff --git a/io.go b/io.go index f22e20c..cf6423f 100644 --- a/io.go +++ b/io.go @@ -165,11 +165,17 @@ type MockMedium struct { ModTimes map[string]time.Time } -var _ Medium = (*MockMedium)(nil) +// Example: medium := io.NewMemoryMedium() +// _ = medium.Write("config/app.yaml", "port: 8080") +type MemoryMedium = MockMedium +var _ Medium = (*MemoryMedium)(nil) + +// NewMockMedium returns MemoryMedium for compatibility. +// // Example: medium := io.NewMockMedium() // _ = medium.Write("config/app.yaml", "port: 8080") -func NewMockMedium() *MockMedium { +func NewMockMedium() *MemoryMedium { return &MockMedium{ Files: make(map[string]string), Dirs: make(map[string]bool), @@ -177,6 +183,12 @@ func NewMockMedium() *MockMedium { } } +// Example: medium := io.NewMemoryMedium() +// _ = medium.Write("config/app.yaml", "port: 8080") +func NewMemoryMedium() *MemoryMedium { + return NewMockMedium() +} + func (medium *MockMedium) Read(path string) (string, error) { content, ok := medium.Files[path] if !ok { @@ -369,6 +381,9 @@ type MockFile struct { offset int64 } +// MemoryFile is the preferred alias for MockFile. +type MemoryFile = MockFile + func (file *MockFile) Stat() (fs.FileInfo, error) { return FileInfo{ name: file.name, @@ -396,6 +411,9 @@ type MockWriteCloser struct { data []byte } +// MemoryWriteCloser is the preferred alias for MockWriteCloser. +type MemoryWriteCloser = MockWriteCloser + func (writeCloser *MockWriteCloser) Write(data []byte) (int, error) { writeCloser.data = append(writeCloser.data, data...) return len(data), nil diff --git a/node/node.go b/node/node.go index 0dc308b..8b1c04e 100644 --- a/node/node.go +++ b/node/node.go @@ -209,7 +209,7 @@ func (node *Node) CopyFile(sourcePath, destinationPath string, perm fs.FileMode) return coreio.Local.WriteMode(destinationPath, string(file.content), perm) } -// Example: _ = nodeTree.CopyTo(io.NewMockMedium(), "config", "backup/config") +// Example: _ = nodeTree.CopyTo(io.NewMemoryMedium(), "config", "backup/config") func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error { sourcePath = core.TrimPrefix(sourcePath, "/") info, err := node.Stat(sourcePath) diff --git a/node/node_test.go b/node/node_test.go index 0580ecb..6918e0e 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -420,12 +420,12 @@ func TestNode_CopyTo_Good(t *testing.T) { n.AddData("config/app.yaml", []byte("port: 8080")) n.AddData("config/env/app.env", []byte("MODE=test")) - fileTarget := coreio.NewMockMedium() + fileTarget := coreio.NewMemoryMedium() err := n.CopyTo(fileTarget, "config/app.yaml", "backup/app.yaml") require.NoError(t, err) assert.Equal(t, "port: 8080", fileTarget.Files["backup/app.yaml"]) - dirTarget := coreio.NewMockMedium() + dirTarget := coreio.NewMemoryMedium() err = n.CopyTo(dirTarget, "config", "backup/config") require.NoError(t, err) assert.Equal(t, "port: 8080", dirTarget.Files["backup/config/app.yaml"]) @@ -434,7 +434,7 @@ func TestNode_CopyTo_Good(t *testing.T) { func TestNode_CopyTo_Bad(t *testing.T) { n := New() - err := n.CopyTo(coreio.NewMockMedium(), "missing", "backup/missing") + err := n.CopyTo(coreio.NewMemoryMedium(), "missing", "backup/missing") assert.Error(t, err) } diff --git a/workspace/service.go b/workspace/service.go index d382d3a..ea3179f 100644 --- a/workspace/service.go +++ b/workspace/service.go @@ -34,12 +34,11 @@ type Options struct { // Example: service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider}) type Service struct { - core *core.Core crypt CryptProvider activeWorkspaceID string rootPath string medium io.Medium - mu sync.RWMutex + lock sync.RWMutex } var _ Workspace = (*Service)(nil) @@ -58,7 +57,6 @@ func New(options Options) (*Service, error) { } service := &Service{ - core: options.Core, rootPath: rootPath, medium: io.Local, } @@ -76,8 +74,8 @@ func New(options Options) (*Service, error) { // Example: workspaceID, _ := service.CreateWorkspace("alice", "pass123") func (service *Service) CreateWorkspace(identifier, password string) (string, error) { - service.mu.Lock() - defer service.mu.Unlock() + service.lock.Lock() + defer service.lock.Unlock() if service.crypt == nil { return "", core.E("workspace.CreateWorkspace", "crypt service not available", nil) @@ -114,8 +112,8 @@ func (service *Service) CreateWorkspace(identifier, password string) (string, er // Example: _ = service.SwitchWorkspace(workspaceID) func (service *Service) SwitchWorkspace(workspaceID string) error { - service.mu.Lock() - defer service.mu.Unlock() + service.lock.Lock() + defer service.lock.Unlock() workspaceDirectory, err := service.resolveWorkspaceDirectory("workspace.SwitchWorkspace", workspaceID) if err != nil { @@ -148,8 +146,8 @@ func (service *Service) resolveActiveWorkspaceFilePath(operation, workspaceFileP // Example: content, _ := service.WorkspaceFileGet("notes/todo.txt") func (service *Service) WorkspaceFileGet(workspaceFilePath string) (string, error) { - service.mu.RLock() - defer service.mu.RUnlock() + service.lock.RLock() + defer service.lock.RUnlock() filePath, err := service.resolveActiveWorkspaceFilePath("workspace.WorkspaceFileGet", workspaceFilePath) if err != nil { @@ -160,8 +158,8 @@ func (service *Service) WorkspaceFileGet(workspaceFilePath string) (string, erro // Example: _ = service.WorkspaceFileSet("notes/todo.txt", "ship it") func (service *Service) WorkspaceFileSet(workspaceFilePath, content string) error { - service.mu.Lock() - defer service.mu.Unlock() + service.lock.Lock() + defer service.lock.Unlock() filePath, err := service.resolveActiveWorkspaceFilePath("workspace.WorkspaceFileSet", workspaceFilePath) if err != nil {