refactor(ax): clarify core storage names
Some checks failed
CI / auto-fix (push) Failing after 0s
CI / test (push) Failing after 2s
CI / auto-merge (push) Failing after 0s

This commit is contained in:
Virgil 2026-03-30 21:39:03 +00:00
parent a8eaaa1581
commit bab889e9ac
13 changed files with 704 additions and 723 deletions

View file

@ -62,10 +62,10 @@ func FromTar(data []byte) (*Medium, error) {
}
// Example: snapshot, _ := medium.Snapshot()
func (m *Medium) Snapshot() ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
data, err := m.dataNode.ToTar()
func (medium *Medium) Snapshot() ([]byte, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
data, err := medium.dataNode.ToTar()
if err != nil {
return nil, core.E("datanode.Snapshot", "tar failed", err)
}
@ -73,23 +73,23 @@ func (m *Medium) Snapshot() ([]byte, error) {
}
// Example: _ = medium.Restore(snapshot)
func (m *Medium) Restore(data []byte) error {
func (medium *Medium) Restore(data []byte) error {
dataNode, err := borgdatanode.FromTar(data)
if err != nil {
return core.E("datanode.Restore", "tar failed", err)
}
m.mu.Lock()
defer m.mu.Unlock()
m.dataNode = dataNode
m.directorySet = make(map[string]bool)
medium.mu.Lock()
defer medium.mu.Unlock()
medium.dataNode = dataNode
medium.directorySet = make(map[string]bool)
return nil
}
// Example: dataNode := medium.DataNode()
func (m *Medium) DataNode() *borgdatanode.DataNode {
m.mu.RLock()
defer m.mu.RUnlock()
return m.dataNode
func (medium *Medium) DataNode() *borgdatanode.DataNode {
medium.mu.RLock()
defer medium.mu.RUnlock()
return medium.dataNode
}
// normaliseEntryPath normalises a path: strips the leading slash and cleans traversal.
@ -104,12 +104,12 @@ func normaliseEntryPath(filePath string) string {
// --- io.Medium interface ---
func (m *Medium) Read(filePath string) (string, error) {
m.mu.RLock()
defer m.mu.RUnlock()
func (medium *Medium) Read(filePath string) (string, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
filePath = normaliseEntryPath(filePath)
f, err := m.dataNode.Open(filePath)
f, err := medium.dataNode.Open(filePath)
if err != nil {
return "", core.E("datanode.Read", core.Concat("not found: ", filePath), fs.ErrNotExist)
}
@ -130,42 +130,42 @@ func (m *Medium) Read(filePath string) (string, error) {
return string(data), nil
}
func (m *Medium) Write(filePath, content string) error {
m.mu.Lock()
defer m.mu.Unlock()
func (medium *Medium) Write(filePath, content string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return core.E("datanode.Write", "empty path", fs.ErrInvalid)
}
m.dataNode.AddData(filePath, []byte(content))
medium.dataNode.AddData(filePath, []byte(content))
// ensure parent directories are tracked
m.ensureDirsLocked(path.Dir(filePath))
medium.ensureDirsLocked(path.Dir(filePath))
return nil
}
func (m *Medium) WriteMode(filePath, content string, mode fs.FileMode) error {
return m.Write(filePath, content)
func (medium *Medium) WriteMode(filePath, content string, mode fs.FileMode) error {
return medium.Write(filePath, content)
}
func (m *Medium) EnsureDir(filePath string) error {
m.mu.Lock()
defer m.mu.Unlock()
func (medium *Medium) EnsureDir(filePath string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return nil
}
m.ensureDirsLocked(filePath)
medium.ensureDirsLocked(filePath)
return nil
}
// ensureDirsLocked marks a directory and all ancestors as existing.
// Caller must hold m.mu.
func (m *Medium) ensureDirsLocked(directoryPath string) {
// Caller must hold medium.mu.
func (medium *Medium) ensureDirsLocked(directoryPath string) {
for directoryPath != "" && directoryPath != "." {
m.directorySet[directoryPath] = true
medium.directorySet[directoryPath] = true
directoryPath = path.Dir(directoryPath)
if directoryPath == "." {
break
@ -173,26 +173,26 @@ func (m *Medium) ensureDirsLocked(directoryPath string) {
}
}
func (m *Medium) IsFile(filePath string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
func (medium *Medium) IsFile(filePath string) bool {
medium.mu.RLock()
defer medium.mu.RUnlock()
filePath = normaliseEntryPath(filePath)
info, err := m.dataNode.Stat(filePath)
info, err := medium.dataNode.Stat(filePath)
return err == nil && !info.IsDir()
}
func (m *Medium) FileGet(filePath string) (string, error) {
return m.Read(filePath)
func (medium *Medium) FileGet(filePath string) (string, error) {
return medium.Read(filePath)
}
func (m *Medium) FileSet(filePath, content string) error {
return m.Write(filePath, content)
func (medium *Medium) FileSet(filePath, content string) error {
return medium.Write(filePath, content)
}
func (m *Medium) Delete(filePath string) error {
m.mu.Lock()
defer m.mu.Unlock()
func (medium *Medium) Delete(filePath string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
@ -200,46 +200,46 @@ func (m *Medium) Delete(filePath string) error {
}
// Check if it's a file in the DataNode
info, err := m.dataNode.Stat(filePath)
info, err := medium.dataNode.Stat(filePath)
if err != nil {
// Check explicit directories
if m.directorySet[filePath] {
if medium.directorySet[filePath] {
// Check if dir is empty
hasChildren, err := m.hasPrefixLocked(filePath + "/")
hasChildren, err := medium.hasPrefixLocked(filePath + "/")
if err != nil {
return core.E("datanode.Delete", core.Concat("failed to inspect directory: ", filePath), err)
}
if hasChildren {
return core.E("datanode.Delete", core.Concat("directory not empty: ", filePath), fs.ErrExist)
}
delete(m.directorySet, filePath)
delete(medium.directorySet, filePath)
return nil
}
return core.E("datanode.Delete", core.Concat("not found: ", filePath), fs.ErrNotExist)
}
if info.IsDir() {
hasChildren, err := m.hasPrefixLocked(filePath + "/")
hasChildren, err := medium.hasPrefixLocked(filePath + "/")
if err != nil {
return core.E("datanode.Delete", core.Concat("failed to inspect directory: ", filePath), err)
}
if hasChildren {
return core.E("datanode.Delete", core.Concat("directory not empty: ", filePath), fs.ErrExist)
}
delete(m.directorySet, filePath)
delete(medium.directorySet, filePath)
return nil
}
// Remove the file by creating a new DataNode without it
if err := m.removeFileLocked(filePath); err != nil {
if err := medium.removeFileLocked(filePath); err != nil {
return core.E("datanode.Delete", core.Concat("failed to delete file: ", filePath), err)
}
return nil
}
func (m *Medium) DeleteAll(filePath string) error {
m.mu.Lock()
defer m.mu.Unlock()
func (medium *Medium) DeleteAll(filePath string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
@ -250,22 +250,22 @@ func (m *Medium) DeleteAll(filePath string) error {
found := false
// Check if filePath itself is a file
info, err := m.dataNode.Stat(filePath)
info, err := medium.dataNode.Stat(filePath)
if err == nil && !info.IsDir() {
if err := m.removeFileLocked(filePath); err != nil {
if err := medium.removeFileLocked(filePath); err != nil {
return core.E("datanode.DeleteAll", core.Concat("failed to delete file: ", filePath), err)
}
found = true
}
// Remove all files under prefix
entries, err := m.collectAllLocked()
entries, err := medium.collectAllLocked()
if err != nil {
return core.E("datanode.DeleteAll", core.Concat("failed to inspect tree: ", filePath), err)
}
for _, name := range entries {
if name == filePath || core.HasPrefix(name, prefix) {
if err := m.removeFileLocked(name); err != nil {
if err := medium.removeFileLocked(name); err != nil {
return core.E("datanode.DeleteAll", core.Concat("failed to delete file: ", name), err)
}
found = true
@ -273,9 +273,9 @@ func (m *Medium) DeleteAll(filePath string) error {
}
// Remove explicit directories under prefix
for directoryPath := range m.directorySet {
for directoryPath := range medium.directorySet {
if directoryPath == filePath || core.HasPrefix(directoryPath, prefix) {
delete(m.directorySet, directoryPath)
delete(medium.directorySet, directoryPath)
found = true
}
}
@ -286,28 +286,28 @@ func (m *Medium) DeleteAll(filePath string) error {
return nil
}
func (m *Medium) Rename(oldPath, newPath string) error {
m.mu.Lock()
defer m.mu.Unlock()
func (medium *Medium) Rename(oldPath, newPath string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
oldPath = normaliseEntryPath(oldPath)
newPath = normaliseEntryPath(newPath)
// Check if source is a file
info, err := m.dataNode.Stat(oldPath)
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 := m.readFileLocked(oldPath)
data, err := medium.readFileLocked(oldPath)
if err != nil {
return core.E("datanode.Rename", core.Concat("failed to read source file: ", oldPath), err)
}
m.dataNode.AddData(newPath, data)
m.ensureDirsLocked(path.Dir(newPath))
if err := m.removeFileLocked(oldPath); err != nil {
medium.dataNode.AddData(newPath, data)
medium.ensureDirsLocked(path.Dir(newPath))
if err := medium.removeFileLocked(oldPath); err != nil {
return core.E("datanode.Rename", core.Concat("failed to remove source file: ", oldPath), err)
}
return nil
@ -317,19 +317,19 @@ func (m *Medium) Rename(oldPath, newPath string) error {
oldPrefix := oldPath + "/"
newPrefix := newPath + "/"
entries, err := m.collectAllLocked()
entries, err := medium.collectAllLocked()
if err != nil {
return core.E("datanode.Rename", core.Concat("failed to inspect tree: ", oldPath), err)
}
for _, name := range entries {
if core.HasPrefix(name, oldPrefix) {
newName := core.Concat(newPrefix, core.TrimPrefix(name, oldPrefix))
data, err := m.readFileLocked(name)
data, err := medium.readFileLocked(name)
if err != nil {
return core.E("datanode.Rename", core.Concat("failed to read source file: ", name), err)
}
m.dataNode.AddData(newName, data)
if err := m.removeFileLocked(name); err != nil {
medium.dataNode.AddData(newName, data)
if err := medium.removeFileLocked(name); err != nil {
return core.E("datanode.Rename", core.Concat("failed to remove source file: ", name), err)
}
}
@ -337,30 +337,30 @@ func (m *Medium) Rename(oldPath, newPath string) error {
// Move explicit directories
dirsToMove := make(map[string]string)
for d := range m.directorySet {
for d := range medium.directorySet {
if d == oldPath || core.HasPrefix(d, oldPrefix) {
newD := core.Concat(newPath, core.TrimPrefix(d, oldPath))
dirsToMove[d] = newD
}
}
for old, nw := range dirsToMove {
delete(m.directorySet, old)
m.directorySet[nw] = true
delete(medium.directorySet, old)
medium.directorySet[nw] = true
}
return nil
}
func (m *Medium) List(filePath string) ([]fs.DirEntry, error) {
m.mu.RLock()
defer m.mu.RUnlock()
func (medium *Medium) List(filePath string) ([]fs.DirEntry, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
filePath = normaliseEntryPath(filePath)
entries, err := m.dataNode.ReadDir(filePath)
entries, err := medium.dataNode.ReadDir(filePath)
if err != nil {
// Check explicit directories
if filePath == "" || m.directorySet[filePath] {
if filePath == "" || medium.directorySet[filePath] {
return []fs.DirEntry{}, nil
}
return nil, core.E("datanode.List", core.Concat("not found: ", filePath), fs.ErrNotExist)
@ -376,7 +376,7 @@ func (m *Medium) List(filePath string) ([]fs.DirEntry, error) {
seen[e.Name()] = true
}
for d := range m.directorySet {
for d := range medium.directorySet {
if !core.HasPrefix(d, prefix) {
continue
}
@ -398,43 +398,43 @@ func (m *Medium) List(filePath string) ([]fs.DirEntry, error) {
return entries, nil
}
func (m *Medium) Stat(filePath string) (fs.FileInfo, error) {
m.mu.RLock()
defer m.mu.RUnlock()
func (medium *Medium) Stat(filePath string) (fs.FileInfo, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return &fileInfo{name: ".", isDir: true, mode: fs.ModeDir | 0755}, nil
}
info, err := m.dataNode.Stat(filePath)
info, err := medium.dataNode.Stat(filePath)
if err == nil {
return info, nil
}
if m.directorySet[filePath] {
if medium.directorySet[filePath] {
return &fileInfo{name: path.Base(filePath), isDir: true, mode: fs.ModeDir | 0755}, nil
}
return nil, core.E("datanode.Stat", core.Concat("not found: ", filePath), fs.ErrNotExist)
}
func (m *Medium) Open(filePath string) (fs.File, error) {
m.mu.RLock()
defer m.mu.RUnlock()
func (medium *Medium) Open(filePath string) (fs.File, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
filePath = normaliseEntryPath(filePath)
return m.dataNode.Open(filePath)
return medium.dataNode.Open(filePath)
}
func (m *Medium) Create(filePath string) (goio.WriteCloser, error) {
func (medium *Medium) Create(filePath string) (goio.WriteCloser, error) {
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return nil, core.E("datanode.Create", "empty path", fs.ErrInvalid)
}
return &writeCloser{medium: m, path: filePath}, nil
return &writeCloser{medium: medium, path: filePath}, nil
}
func (m *Medium) Append(filePath string) (goio.WriteCloser, error) {
func (medium *Medium) Append(filePath string) (goio.WriteCloser, error) {
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return nil, core.E("datanode.Append", "empty path", fs.ErrInvalid)
@ -442,71 +442,71 @@ func (m *Medium) Append(filePath string) (goio.WriteCloser, error) {
// Read existing content
var existing []byte
m.mu.RLock()
if m.IsFile(filePath) {
data, err := m.readFileLocked(filePath)
medium.mu.RLock()
if medium.IsFile(filePath) {
data, err := medium.readFileLocked(filePath)
if err != nil {
m.mu.RUnlock()
medium.mu.RUnlock()
return nil, core.E("datanode.Append", core.Concat("failed to read existing content: ", filePath), err)
}
existing = data
}
m.mu.RUnlock()
medium.mu.RUnlock()
return &writeCloser{medium: m, path: filePath, buf: existing}, nil
return &writeCloser{medium: medium, path: filePath, buf: existing}, nil
}
func (m *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
m.mu.RLock()
defer m.mu.RUnlock()
func (medium *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
filePath = normaliseEntryPath(filePath)
f, err := m.dataNode.Open(filePath)
f, err := medium.dataNode.Open(filePath)
if err != nil {
return nil, core.E("datanode.ReadStream", core.Concat("not found: ", filePath), fs.ErrNotExist)
}
return f.(goio.ReadCloser), nil
}
func (m *Medium) WriteStream(filePath string) (goio.WriteCloser, error) {
return m.Create(filePath)
func (medium *Medium) WriteStream(filePath string) (goio.WriteCloser, error) {
return medium.Create(filePath)
}
func (m *Medium) Exists(filePath string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
func (medium *Medium) Exists(filePath string) bool {
medium.mu.RLock()
defer medium.mu.RUnlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return true // root always exists
}
_, err := m.dataNode.Stat(filePath)
_, err := medium.dataNode.Stat(filePath)
if err == nil {
return true
}
return m.directorySet[filePath]
return medium.directorySet[filePath]
}
func (m *Medium) IsDir(filePath string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
func (medium *Medium) IsDir(filePath string) bool {
medium.mu.RLock()
defer medium.mu.RUnlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return true
}
info, err := m.dataNode.Stat(filePath)
info, err := medium.dataNode.Stat(filePath)
if err == nil {
return info.IsDir()
}
return m.directorySet[filePath]
return medium.directorySet[filePath]
}
// --- internal helpers ---
// hasPrefixLocked checks if any file path starts with prefix. Caller holds lock.
func (m *Medium) hasPrefixLocked(prefix string) (bool, error) {
entries, err := m.collectAllLocked()
func (medium *Medium) hasPrefixLocked(prefix string) (bool, error) {
entries, err := medium.collectAllLocked()
if err != nil {
return false, err
}
@ -515,7 +515,7 @@ func (m *Medium) hasPrefixLocked(prefix string) (bool, error) {
return true, nil
}
}
for d := range m.directorySet {
for d := range medium.directorySet {
if core.HasPrefix(d, prefix) {
return true, nil
}
@ -524,9 +524,9 @@ func (m *Medium) hasPrefixLocked(prefix string) (bool, error) {
}
// collectAllLocked returns all file paths in the DataNode. Caller holds lock.
func (m *Medium) collectAllLocked() ([]string, error) {
func (medium *Medium) collectAllLocked() ([]string, error) {
var names []string
err := dataNodeWalkDir(m.dataNode, ".", func(filePath string, entry fs.DirEntry, err error) error {
err := dataNodeWalkDir(medium.dataNode, ".", func(filePath string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
@ -538,8 +538,8 @@ func (m *Medium) collectAllLocked() ([]string, error) {
return names, err
}
func (m *Medium) readFileLocked(name string) ([]byte, error) {
f, err := dataNodeOpen(m.dataNode, name)
func (medium *Medium) readFileLocked(name string) ([]byte, error) {
f, err := dataNodeOpen(medium.dataNode, name)
if err != nil {
return nil, err
}
@ -556,9 +556,9 @@ func (m *Medium) readFileLocked(name 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 m.mu write lock.
func (m *Medium) removeFileLocked(target string) error {
entries, err := m.collectAllLocked()
// Caller must hold medium.mu write lock.
func (medium *Medium) removeFileLocked(target string) error {
entries, err := medium.collectAllLocked()
if err != nil {
return err
}
@ -567,13 +567,13 @@ func (m *Medium) removeFileLocked(target string) error {
if name == target {
continue
}
data, err := m.readFileLocked(name)
data, err := medium.readFileLocked(name)
if err != nil {
return err
}
newDN.AddData(name, data)
}
m.dataNode = newDN
medium.dataNode = newDN
return nil
}

2
doc.go
View file

@ -1,4 +1,4 @@
// Package io gives CoreGO a single storage surface.
// Package io exposes CoreGO's storage surface.
//
// medium, _ := io.NewSandboxed("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")

284
io.go
View file

@ -59,7 +59,7 @@ type Medium interface {
IsDir(path string) bool
}
// FileInfo is a test helper that satisfies fs.FileInfo.
// Example: info := io.FileInfo{name: "app.yaml", size: 8, mode: 0644}
type FileInfo struct {
name string
size int64
@ -68,19 +68,19 @@ type FileInfo struct {
isDir bool
}
func (fi FileInfo) Name() string { return fi.name }
func (info FileInfo) Name() string { return info.name }
func (fi FileInfo) Size() int64 { return fi.size }
func (info FileInfo) Size() int64 { return info.size }
func (fi FileInfo) Mode() fs.FileMode { return fi.mode }
func (info FileInfo) Mode() fs.FileMode { return info.mode }
func (fi FileInfo) ModTime() time.Time { return fi.modTime }
func (info FileInfo) ModTime() time.Time { return info.modTime }
func (fi FileInfo) IsDir() bool { return fi.isDir }
func (info FileInfo) IsDir() bool { return info.isDir }
func (fi FileInfo) Sys() any { return nil }
func (info FileInfo) Sys() any { return nil }
// DirEntry is a test helper that satisfies fs.DirEntry.
// Example: entry := io.DirEntry{name: "app.yaml", mode: 0644}
type DirEntry struct {
name string
isDir bool
@ -88,15 +88,15 @@ type DirEntry struct {
info fs.FileInfo
}
func (de DirEntry) Name() string { return de.name }
func (entry DirEntry) Name() string { return entry.name }
func (de DirEntry) IsDir() bool { return de.isDir }
func (entry DirEntry) IsDir() bool { return entry.isDir }
func (de DirEntry) Type() fs.FileMode { return de.mode.Type() }
func (entry DirEntry) Type() fs.FileMode { return entry.mode.Type() }
func (de DirEntry) Info() (fs.FileInfo, error) { return de.info, nil }
func (entry DirEntry) Info() (fs.FileInfo, error) { return entry.info, nil }
// Example: io.Local.Read("/etc/hostname")
// Example: _ = io.Local.Read("/etc/hostname")
var Local Medium
var _ Medium = (*local.Medium)(nil)
@ -115,36 +115,34 @@ func NewSandboxed(root string) (Medium, error) {
return local.New(root)
}
// --- Helper Functions ---
// Example: content, _ := io.Read(medium, "config/app.yaml")
func Read(m Medium, path string) (string, error) {
return m.Read(path)
func Read(medium Medium, path string) (string, error) {
return medium.Read(path)
}
// Example: _ = io.Write(medium, "config/app.yaml", "port: 8080")
func Write(m Medium, path, content string) error {
return m.Write(path, content)
func Write(medium Medium, path, content string) error {
return medium.Write(path, content)
}
// Example: reader, _ := io.ReadStream(medium, "logs/app.log")
func ReadStream(m Medium, path string) (goio.ReadCloser, error) {
return m.ReadStream(path)
func ReadStream(medium Medium, path string) (goio.ReadCloser, error) {
return medium.ReadStream(path)
}
// Example: writer, _ := io.WriteStream(medium, "logs/app.log")
func WriteStream(m Medium, path string) (goio.WriteCloser, error) {
return m.WriteStream(path)
func WriteStream(medium Medium, path string) (goio.WriteCloser, error) {
return medium.WriteStream(path)
}
// Example: _ = io.EnsureDir(medium, "config")
func EnsureDir(m Medium, path string) error {
return m.EnsureDir(path)
func EnsureDir(medium Medium, path string) error {
return medium.EnsureDir(path)
}
// Example: ok := io.IsFile(medium, "config/app.yaml")
func IsFile(m Medium, path string) bool {
return m.IsFile(path)
func IsFile(medium Medium, path string) bool {
return medium.IsFile(path)
}
// Example: _ = io.Copy(source, "input.txt", destination, "backup/input.txt")
@ -159,8 +157,6 @@ func Copy(source Medium, sourcePath string, destination Medium, destinationPath
return nil
}
// --- MockMedium ---
// Example: medium := io.NewMockMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
type MockMedium struct {
@ -181,94 +177,91 @@ func NewMockMedium() *MockMedium {
}
}
func (m *MockMedium) Read(path string) (string, error) {
content, ok := m.Files[path]
func (medium *MockMedium) Read(path string) (string, error) {
content, ok := medium.Files[path]
if !ok {
return "", core.E("io.MockMedium.Read", core.Concat("file not found: ", path), fs.ErrNotExist)
}
return content, nil
}
func (m *MockMedium) Write(path, content string) error {
m.Files[path] = content
m.ModTimes[path] = time.Now()
func (medium *MockMedium) Write(path, content string) error {
medium.Files[path] = content
medium.ModTimes[path] = time.Now()
return nil
}
func (m *MockMedium) WriteMode(path, content string, mode fs.FileMode) error {
return m.Write(path, content)
func (medium *MockMedium) WriteMode(path, content string, mode fs.FileMode) error {
return medium.Write(path, content)
}
func (m *MockMedium) EnsureDir(path string) error {
m.Dirs[path] = true
func (medium *MockMedium) EnsureDir(path string) error {
medium.Dirs[path] = true
return nil
}
func (m *MockMedium) IsFile(path string) bool {
_, ok := m.Files[path]
func (medium *MockMedium) IsFile(path string) bool {
_, ok := medium.Files[path]
return ok
}
func (m *MockMedium) FileGet(path string) (string, error) {
return m.Read(path)
func (medium *MockMedium) FileGet(path string) (string, error) {
return medium.Read(path)
}
func (m *MockMedium) FileSet(path, content string) error {
return m.Write(path, content)
func (medium *MockMedium) FileSet(path, content string) error {
return medium.Write(path, content)
}
func (m *MockMedium) Delete(path string) error {
if _, ok := m.Files[path]; ok {
delete(m.Files, path)
func (medium *MockMedium) Delete(path string) error {
if _, ok := medium.Files[path]; ok {
delete(medium.Files, path)
return nil
}
if _, ok := m.Dirs[path]; ok {
// Check if directory is empty (no files or subdirs with this prefix)
if _, ok := medium.Dirs[path]; ok {
prefix := path
if !core.HasSuffix(prefix, "/") {
prefix += "/"
}
for f := range m.Files {
if core.HasPrefix(f, prefix) {
for filePath := range medium.Files {
if core.HasPrefix(filePath, prefix) {
return core.E("io.MockMedium.Delete", core.Concat("directory not empty: ", path), fs.ErrExist)
}
}
for d := range m.Dirs {
if d != path && core.HasPrefix(d, prefix) {
for directoryPath := range medium.Dirs {
if directoryPath != path && core.HasPrefix(directoryPath, prefix) {
return core.E("io.MockMedium.Delete", core.Concat("directory not empty: ", path), fs.ErrExist)
}
}
delete(m.Dirs, path)
delete(medium.Dirs, path)
return nil
}
return core.E("io.MockMedium.Delete", core.Concat("path not found: ", path), fs.ErrNotExist)
}
func (m *MockMedium) DeleteAll(path string) error {
func (medium *MockMedium) DeleteAll(path string) error {
found := false
if _, ok := m.Files[path]; ok {
delete(m.Files, path)
if _, ok := medium.Files[path]; ok {
delete(medium.Files, path)
found = true
}
if _, ok := m.Dirs[path]; ok {
delete(m.Dirs, path)
if _, ok := medium.Dirs[path]; ok {
delete(medium.Dirs, path)
found = true
}
// Delete all entries under this path
prefix := path
if !core.HasSuffix(prefix, "/") {
prefix += "/"
}
for f := range m.Files {
if core.HasPrefix(f, prefix) {
delete(m.Files, f)
for filePath := range medium.Files {
if core.HasPrefix(filePath, prefix) {
delete(medium.Files, filePath)
found = true
}
}
for d := range m.Dirs {
if core.HasPrefix(d, prefix) {
delete(m.Dirs, d)
for directoryPath := range medium.Dirs {
if core.HasPrefix(directoryPath, prefix) {
delete(medium.Dirs, directoryPath)
found = true
}
}
@ -279,20 +272,19 @@ func (m *MockMedium) DeleteAll(path string) error {
return nil
}
func (m *MockMedium) Rename(oldPath, newPath string) error {
if content, ok := m.Files[oldPath]; ok {
m.Files[newPath] = content
delete(m.Files, oldPath)
if mt, ok := m.ModTimes[oldPath]; ok {
m.ModTimes[newPath] = mt
delete(m.ModTimes, oldPath)
func (medium *MockMedium) Rename(oldPath, newPath string) error {
if content, ok := medium.Files[oldPath]; ok {
medium.Files[newPath] = content
delete(medium.Files, oldPath)
if modTime, ok := medium.ModTimes[oldPath]; ok {
medium.ModTimes[newPath] = modTime
delete(medium.ModTimes, oldPath)
}
return nil
}
if _, ok := m.Dirs[oldPath]; ok {
// Move directory and all contents
m.Dirs[newPath] = true
delete(m.Dirs, oldPath)
if _, ok := medium.Dirs[oldPath]; ok {
medium.Dirs[newPath] = true
delete(medium.Dirs, oldPath)
oldPrefix := oldPath
if !core.HasSuffix(oldPrefix, "/") {
@ -303,42 +295,40 @@ func (m *MockMedium) Rename(oldPath, newPath string) error {
newPrefix += "/"
}
// Collect files to move first (don't mutate during iteration)
filesToMove := make(map[string]string)
for f := range m.Files {
if core.HasPrefix(f, oldPrefix) {
newF := core.Concat(newPrefix, core.TrimPrefix(f, oldPrefix))
filesToMove[f] = newF
for filePath := range medium.Files {
if core.HasPrefix(filePath, oldPrefix) {
newFilePath := core.Concat(newPrefix, core.TrimPrefix(filePath, oldPrefix))
filesToMove[filePath] = newFilePath
}
}
for oldF, newF := range filesToMove {
m.Files[newF] = m.Files[oldF]
delete(m.Files, oldF)
if mt, ok := m.ModTimes[oldF]; ok {
m.ModTimes[newF] = mt
delete(m.ModTimes, oldF)
for oldFilePath, newFilePath := range filesToMove {
medium.Files[newFilePath] = medium.Files[oldFilePath]
delete(medium.Files, oldFilePath)
if modTime, ok := medium.ModTimes[oldFilePath]; ok {
medium.ModTimes[newFilePath] = modTime
delete(medium.ModTimes, oldFilePath)
}
}
// Collect directories to move first
dirsToMove := make(map[string]string)
for d := range m.Dirs {
if core.HasPrefix(d, oldPrefix) {
newD := core.Concat(newPrefix, core.TrimPrefix(d, oldPrefix))
dirsToMove[d] = newD
for directoryPath := range medium.Dirs {
if core.HasPrefix(directoryPath, oldPrefix) {
newDirectoryPath := core.Concat(newPrefix, core.TrimPrefix(directoryPath, oldPrefix))
dirsToMove[directoryPath] = newDirectoryPath
}
}
for oldD, newD := range dirsToMove {
m.Dirs[newD] = true
delete(m.Dirs, oldD)
for oldDirectoryPath, newDirectoryPath := range dirsToMove {
medium.Dirs[newDirectoryPath] = true
delete(medium.Dirs, oldDirectoryPath)
}
return nil
}
return core.E("io.MockMedium.Rename", core.Concat("path not found: ", oldPath), fs.ErrNotExist)
}
func (m *MockMedium) Open(path string) (fs.File, error) {
content, ok := m.Files[path]
func (medium *MockMedium) Open(path string) (fs.File, error) {
content, ok := medium.Files[path]
if !ok {
return nil, core.E("io.MockMedium.Open", core.Concat("file not found: ", path), fs.ErrNotExist)
}
@ -348,28 +338,28 @@ func (m *MockMedium) Open(path string) (fs.File, error) {
}, nil
}
func (m *MockMedium) Create(path string) (goio.WriteCloser, error) {
func (medium *MockMedium) Create(path string) (goio.WriteCloser, error) {
return &MockWriteCloser{
medium: m,
medium: medium,
path: path,
}, nil
}
func (m *MockMedium) Append(path string) (goio.WriteCloser, error) {
content := m.Files[path]
func (medium *MockMedium) Append(path string) (goio.WriteCloser, error) {
content := medium.Files[path]
return &MockWriteCloser{
medium: m,
medium: medium,
path: path,
data: []byte(content),
}, nil
}
func (m *MockMedium) ReadStream(path string) (goio.ReadCloser, error) {
return m.Open(path)
func (medium *MockMedium) ReadStream(path string) (goio.ReadCloser, error) {
return medium.Open(path)
}
func (m *MockMedium) WriteStream(path string) (goio.WriteCloser, error) {
return m.Create(path)
func (medium *MockMedium) WriteStream(path string) (goio.WriteCloser, error) {
return medium.Create(path)
}
// MockFile implements fs.File for MockMedium.
@ -379,23 +369,23 @@ type MockFile struct {
offset int64
}
func (f *MockFile) Stat() (fs.FileInfo, error) {
func (file *MockFile) Stat() (fs.FileInfo, error) {
return FileInfo{
name: f.name,
size: int64(len(f.content)),
name: file.name,
size: int64(len(file.content)),
}, nil
}
func (f *MockFile) Read(b []byte) (int, error) {
if f.offset >= int64(len(f.content)) {
func (file *MockFile) Read(buffer []byte) (int, error) {
if file.offset >= int64(len(file.content)) {
return 0, goio.EOF
}
n := copy(b, f.content[f.offset:])
f.offset += int64(n)
return n, nil
readCount := copy(buffer, file.content[file.offset:])
file.offset += int64(readCount)
return readCount, nil
}
func (f *MockFile) Close() error {
func (file *MockFile) Close() error {
return nil
}
@ -406,34 +396,33 @@ type MockWriteCloser struct {
data []byte
}
func (w *MockWriteCloser) Write(p []byte) (int, error) {
w.data = append(w.data, p...)
return len(p), nil
func (writeCloser *MockWriteCloser) Write(data []byte) (int, error) {
writeCloser.data = append(writeCloser.data, data...)
return len(data), nil
}
func (w *MockWriteCloser) Close() error {
w.medium.Files[w.path] = string(w.data)
w.medium.ModTimes[w.path] = time.Now()
func (writeCloser *MockWriteCloser) Close() error {
writeCloser.medium.Files[writeCloser.path] = string(writeCloser.data)
writeCloser.medium.ModTimes[writeCloser.path] = time.Now()
return nil
}
func (m *MockMedium) List(path string) ([]fs.DirEntry, error) {
if _, ok := m.Dirs[path]; !ok {
// Check if it's the root or has children
func (medium *MockMedium) List(path string) ([]fs.DirEntry, error) {
if _, ok := medium.Dirs[path]; !ok {
hasChildren := false
prefix := path
if path != "" && !core.HasSuffix(prefix, "/") {
prefix += "/"
}
for f := range m.Files {
if core.HasPrefix(f, prefix) {
for filePath := range medium.Files {
if core.HasPrefix(filePath, prefix) {
hasChildren = true
break
}
}
if !hasChildren {
for d := range m.Dirs {
if core.HasPrefix(d, prefix) {
for directoryPath := range medium.Dirs {
if core.HasPrefix(directoryPath, prefix) {
hasChildren = true
break
}
@ -452,16 +441,13 @@ func (m *MockMedium) List(path string) ([]fs.DirEntry, error) {
seen := make(map[string]bool)
var entries []fs.DirEntry
// Find immediate children (files)
for f, content := range m.Files {
if !core.HasPrefix(f, prefix) {
for filePath, content := range medium.Files {
if !core.HasPrefix(filePath, prefix) {
continue
}
rest := core.TrimPrefix(f, prefix)
rest := core.TrimPrefix(filePath, prefix)
if rest == "" || core.Contains(rest, "/") {
// Skip if it's not an immediate child
if idx := bytes.IndexByte([]byte(rest), '/'); idx != -1 {
// This is a subdirectory
dirName := rest[:idx]
if !seen[dirName] {
seen[dirName] = true
@ -494,16 +480,14 @@ func (m *MockMedium) List(path string) ([]fs.DirEntry, error) {
}
}
// Find immediate subdirectories
for d := range m.Dirs {
if !core.HasPrefix(d, prefix) {
for directoryPath := range medium.Dirs {
if !core.HasPrefix(directoryPath, prefix) {
continue
}
rest := core.TrimPrefix(d, prefix)
rest := core.TrimPrefix(directoryPath, prefix)
if rest == "" {
continue
}
// Get only immediate child
if idx := bytes.IndexByte([]byte(rest), '/'); idx != -1 {
rest = rest[:idx]
}
@ -525,9 +509,9 @@ func (m *MockMedium) List(path string) ([]fs.DirEntry, error) {
return entries, nil
}
func (m *MockMedium) Stat(path string) (fs.FileInfo, error) {
if content, ok := m.Files[path]; ok {
modTime, ok := m.ModTimes[path]
func (medium *MockMedium) Stat(path string) (fs.FileInfo, error) {
if content, ok := medium.Files[path]; ok {
modTime, ok := medium.ModTimes[path]
if !ok {
modTime = time.Now()
}
@ -538,7 +522,7 @@ func (m *MockMedium) Stat(path string) (fs.FileInfo, error) {
modTime: modTime,
}, nil
}
if _, ok := m.Dirs[path]; ok {
if _, ok := medium.Dirs[path]; ok {
return FileInfo{
name: core.PathBase(path),
isDir: true,
@ -548,17 +532,17 @@ func (m *MockMedium) Stat(path string) (fs.FileInfo, error) {
return nil, core.E("io.MockMedium.Stat", core.Concat("path not found: ", path), fs.ErrNotExist)
}
func (m *MockMedium) Exists(path string) bool {
if _, ok := m.Files[path]; ok {
func (medium *MockMedium) Exists(path string) bool {
if _, ok := medium.Files[path]; ok {
return true
}
if _, ok := m.Dirs[path]; ok {
if _, ok := medium.Dirs[path]; ok {
return true
}
return false
}
func (m *MockMedium) IsDir(path string) bool {
_, ok := m.Dirs[path]
func (medium *MockMedium) IsDir(path string) bool {
_, ok := medium.Dirs[path]
return ok
}

View file

@ -177,17 +177,15 @@ func logSandboxEscape(root, path, attempted string) {
core.Security("sandbox escape detected", "root", root, "path", path, "attempted", attempted, "user", username)
}
// sandboxedPath resolves a path inside the filesystem root.
// Absolute paths are sandboxed under root (unless root is "/").
func (m *Medium) sandboxedPath(path string) string {
func (medium *Medium) sandboxedPath(path string) string {
if path == "" {
return m.filesystemRoot
return medium.filesystemRoot
}
// If the path is relative and the medium is rooted at "/",
// treat it as relative to the current working directory.
// This makes io.Local behave more like the standard 'os' package.
if m.filesystemRoot == dirSeparator() && !core.PathIsAbs(normalisePath(path)) {
if medium.filesystemRoot == dirSeparator() && !core.PathIsAbs(normalisePath(path)) {
return core.Path(currentWorkingDir(), normalisePath(path))
}
@ -196,23 +194,22 @@ func (m *Medium) sandboxedPath(path string) string {
clean := cleanSandboxPath(path)
// If root is "/", allow absolute paths through
if m.filesystemRoot == dirSeparator() {
if medium.filesystemRoot == dirSeparator() {
return clean
}
// Join cleaned relative path with root
return core.Path(m.filesystemRoot, core.TrimPrefix(clean, dirSeparator()))
return core.Path(medium.filesystemRoot, core.TrimPrefix(clean, dirSeparator()))
}
// validatePath ensures the path is within the sandbox, following symlinks if they exist.
func (m *Medium) validatePath(path string) (string, error) {
if m.filesystemRoot == dirSeparator() {
return m.sandboxedPath(path), nil
func (medium *Medium) validatePath(path string) (string, error) {
if medium.filesystemRoot == dirSeparator() {
return medium.sandboxedPath(path), nil
}
// Split the cleaned path into components
parts := splitPathParts(cleanSandboxPath(path))
current := m.filesystemRoot
current := medium.filesystemRoot
for _, part := range parts {
next := core.Path(current, part)
@ -229,9 +226,9 @@ func (m *Medium) validatePath(path string) (string, error) {
}
// Verify the resolved part is still within the root
if !isWithinRoot(m.filesystemRoot, realNext) {
if !isWithinRoot(medium.filesystemRoot, realNext) {
// Security event: sandbox escape attempt
logSandboxEscape(m.filesystemRoot, path, realNext)
logSandboxEscape(medium.filesystemRoot, path, realNext)
return "", fs.ErrPermission
}
current = realNext
@ -240,98 +237,98 @@ func (m *Medium) validatePath(path string) (string, error) {
return current, nil
}
func (m *Medium) Read(path string) (string, error) {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) Read(path string) (string, error) {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return "", err
}
return resultString("local.Read", core.Concat("read failed: ", path), unrestrictedFileSystem.Read(resolvedPath))
}
func (m *Medium) Write(path, content string) error {
return m.WriteMode(path, content, 0644)
func (medium *Medium) Write(path, content string) error {
return medium.WriteMode(path, content, 0644)
}
func (m *Medium) WriteMode(path, content string, mode fs.FileMode) error {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) WriteMode(path, content string, mode fs.FileMode) error {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return err
}
return resultError("local.WriteMode", core.Concat("write failed: ", path), unrestrictedFileSystem.WriteMode(resolvedPath, content, mode))
}
func (m *Medium) EnsureDir(path string) error {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) EnsureDir(path string) error {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return err
}
return resultError("local.EnsureDir", core.Concat("ensure dir failed: ", path), unrestrictedFileSystem.EnsureDir(resolvedPath))
}
func (m *Medium) IsDir(path string) bool {
func (medium *Medium) IsDir(path string) bool {
if path == "" {
return false
}
resolvedPath, err := m.validatePath(path)
resolvedPath, err := medium.validatePath(path)
if err != nil {
return false
}
return unrestrictedFileSystem.IsDir(resolvedPath)
}
func (m *Medium) IsFile(path string) bool {
func (medium *Medium) IsFile(path string) bool {
if path == "" {
return false
}
resolvedPath, err := m.validatePath(path)
resolvedPath, err := medium.validatePath(path)
if err != nil {
return false
}
return unrestrictedFileSystem.IsFile(resolvedPath)
}
func (m *Medium) Exists(path string) bool {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) Exists(path string) bool {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return false
}
return unrestrictedFileSystem.Exists(resolvedPath)
}
func (m *Medium) List(path string) ([]fs.DirEntry, error) {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) List(path string) ([]fs.DirEntry, error) {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return nil, err
}
return resultDirEntries("local.List", core.Concat("list failed: ", path), unrestrictedFileSystem.List(resolvedPath))
}
func (m *Medium) Stat(path string) (fs.FileInfo, error) {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) Stat(path string) (fs.FileInfo, error) {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return nil, err
}
return resultFileInfo("local.Stat", core.Concat("stat failed: ", path), unrestrictedFileSystem.Stat(resolvedPath))
}
func (m *Medium) Open(path string) (fs.File, error) {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) Open(path string) (fs.File, error) {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return nil, err
}
return resultFile("local.Open", core.Concat("open failed: ", path), unrestrictedFileSystem.Open(resolvedPath))
}
func (m *Medium) Create(path string) (goio.WriteCloser, error) {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) Create(path string) (goio.WriteCloser, error) {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return nil, err
}
return resultWriteCloser("local.Create", core.Concat("create failed: ", path), unrestrictedFileSystem.Create(resolvedPath))
}
func (m *Medium) Append(path string) (goio.WriteCloser, error) {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) Append(path string) (goio.WriteCloser, error) {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return nil, err
}
@ -339,17 +336,17 @@ func (m *Medium) Append(path string) (goio.WriteCloser, error) {
}
// Example: reader, _ := medium.ReadStream("logs/app.log")
func (m *Medium) ReadStream(path string) (goio.ReadCloser, error) {
return m.Open(path)
func (medium *Medium) ReadStream(path string) (goio.ReadCloser, error) {
return medium.Open(path)
}
// Example: writer, _ := medium.WriteStream("logs/app.log")
func (m *Medium) WriteStream(path string) (goio.WriteCloser, error) {
return m.Create(path)
func (medium *Medium) WriteStream(path string) (goio.WriteCloser, error) {
return medium.Create(path)
}
func (m *Medium) Delete(path string) error {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) Delete(path string) error {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return err
}
@ -359,8 +356,8 @@ func (m *Medium) Delete(path string) error {
return resultError("local.Delete", core.Concat("delete failed: ", path), unrestrictedFileSystem.Delete(resolvedPath))
}
func (m *Medium) DeleteAll(path string) error {
resolvedPath, err := m.validatePath(path)
func (medium *Medium) DeleteAll(path string) error {
resolvedPath, err := medium.validatePath(path)
if err != nil {
return err
}
@ -370,24 +367,24 @@ func (m *Medium) DeleteAll(path string) error {
return resultError("local.DeleteAll", core.Concat("delete all failed: ", path), unrestrictedFileSystem.DeleteAll(resolvedPath))
}
func (m *Medium) Rename(oldPath, newPath string) error {
oldResolvedPath, err := m.validatePath(oldPath)
func (medium *Medium) Rename(oldPath, newPath string) error {
oldResolvedPath, err := medium.validatePath(oldPath)
if err != nil {
return err
}
newResolvedPath, err := m.validatePath(newPath)
newResolvedPath, err := medium.validatePath(newPath)
if err != nil {
return err
}
return resultError("local.Rename", core.Concat("rename failed: ", oldPath), unrestrictedFileSystem.Rename(oldResolvedPath, newResolvedPath))
}
func (m *Medium) FileGet(path string) (string, error) {
return m.Read(path)
func (medium *Medium) FileGet(path string) (string, error) {
return medium.Read(path)
}
func (m *Medium) FileSet(path, content string) error {
return m.Write(path, content)
func (medium *Medium) FileSet(path, content string) error {
return medium.Write(path, content)
}
func lstat(path string) (*syscall.Stat_t, error) {

View file

@ -39,7 +39,7 @@ func New() *Node {
// ---------- Node-specific methods ----------
// AddData stages content in the in-memory filesystem.
func (n *Node) AddData(name string, content []byte) {
func (node *Node) AddData(name string, content []byte) {
name = core.TrimPrefix(name, "/")
if name == "" {
return
@ -48,7 +48,7 @@ func (n *Node) AddData(name string, content []byte) {
if core.HasSuffix(name, "/") {
return
}
n.files[name] = &dataFile{
node.files[name] = &dataFile{
name: name,
content: content,
modTime: time.Now(),
@ -56,11 +56,11 @@ func (n *Node) AddData(name string, content []byte) {
}
// ToTar serialises the entire in-memory tree to a tar archive.
func (n *Node) ToTar() ([]byte, error) {
func (node *Node) ToTar() ([]byte, error) {
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
for _, file := range n.files {
for _, file := range node.files {
hdr := &tar.Header{
Name: file.name,
Mode: 0600,
@ -92,7 +92,7 @@ func FromTar(data []byte) (*Node, error) {
}
// LoadTar replaces the in-memory tree with the contents of a tar archive.
func (n *Node) LoadTar(data []byte) error {
func (node *Node) LoadTar(data []byte) error {
newFiles := make(map[string]*dataFile)
tr := tar.NewReader(bytes.NewReader(data))
@ -122,12 +122,12 @@ func (n *Node) LoadTar(data []byte) error {
}
}
n.files = newFiles
node.files = newFiles
return nil
}
func (n *Node) WalkNode(root string, fn fs.WalkDirFunc) error {
return fs.WalkDir(n, root, fn)
func (node *Node) WalkNode(root string, fn fs.WalkDirFunc) error {
return fs.WalkDir(node, root, fn)
}
// Example: options := node.WalkOptions{MaxDepth: 1, SkipErrors: true}
@ -147,15 +147,15 @@ type WalkOptions struct {
// nodeTree := New()
// options := WalkOptions{MaxDepth: 1, SkipErrors: true}
// _ = nodeTree.WalkWithOptions(".", func(path string, entry fs.DirEntry, err error) error { return nil }, options)
func (n *Node) WalkWithOptions(root string, fn fs.WalkDirFunc, options WalkOptions) error {
func (node *Node) WalkWithOptions(root string, fn fs.WalkDirFunc, options WalkOptions) error {
if options.SkipErrors {
// If root doesn't exist, silently return nil.
if _, err := n.Stat(root); err != nil {
if _, err := node.Stat(root); err != nil {
return nil
}
}
return fs.WalkDir(n, root, func(entryPath string, entry fs.DirEntry, err error) error {
return fs.WalkDir(node, root, func(entryPath string, entry fs.DirEntry, err error) error {
if options.Filter != nil && err == nil {
if !options.Filter(entryPath, entry) {
if entry != nil && entry.IsDir() {
@ -182,9 +182,9 @@ func (n *Node) WalkWithOptions(root string, fn fs.WalkDirFunc, options WalkOptio
})
}
func (n *Node) ReadFile(name string) ([]byte, error) {
func (node *Node) ReadFile(name string) ([]byte, error) {
name = core.TrimPrefix(name, "/")
f, ok := n.files[name]
f, ok := node.files[name]
if !ok {
return nil, core.E("node.ReadFile", core.Concat("path not found: ", name), fs.ErrNotExist)
}
@ -195,12 +195,12 @@ func (n *Node) ReadFile(name string) ([]byte, error) {
}
// CopyFile copies a file from the in-memory tree to the local filesystem.
func (n *Node) CopyFile(sourcePath, destinationPath string, perm fs.FileMode) error {
func (node *Node) CopyFile(sourcePath, destinationPath string, perm fs.FileMode) error {
sourcePath = core.TrimPrefix(sourcePath, "/")
f, ok := n.files[sourcePath]
f, ok := node.files[sourcePath]
if !ok {
// Check if it's a directory — can't copy directories this way.
info, err := n.Stat(sourcePath)
info, err := node.Stat(sourcePath)
if err != nil {
return core.E("node.CopyFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
}
@ -221,17 +221,17 @@ func (n *Node) CopyFile(sourcePath, destinationPath string, perm fs.FileMode) er
// Example usage:
//
// dst := io.NewMockMedium()
// _ = n.CopyTo(dst, "config", "backup/config")
func (n *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error {
// _ = node.CopyTo(dst, "config", "backup/config")
func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error {
sourcePath = core.TrimPrefix(sourcePath, "/")
info, err := n.Stat(sourcePath)
info, err := node.Stat(sourcePath)
if err != nil {
return err
}
if !info.IsDir() {
// Single file copy
f, ok := n.files[sourcePath]
f, ok := node.files[sourcePath]
if !ok {
return core.E("node.CopyTo", core.Concat("path not found: ", sourcePath), fs.ErrNotExist)
}
@ -244,7 +244,7 @@ func (n *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error {
prefix += "/"
}
for filePath, f := range n.files {
for filePath, f := range node.files {
if !core.HasPrefix(filePath, prefix) && filePath != sourcePath {
continue
}
@ -262,9 +262,9 @@ func (n *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error {
// ---------- Medium interface: fs.FS methods ----------
func (n *Node) Open(name string) (fs.File, error) {
func (node *Node) Open(name string) (fs.File, error) {
name = core.TrimPrefix(name, "/")
if file, ok := n.files[name]; ok {
if file, ok := node.files[name]; ok {
return &dataFileReader{file: file}, nil
}
// Check if it's a directory
@ -272,7 +272,7 @@ func (n *Node) Open(name string) (fs.File, error) {
if name == "." || name == "" {
prefix = ""
}
for filePath := range n.files {
for filePath := range node.files {
if core.HasPrefix(filePath, prefix) {
return &dirFile{path: name, modTime: time.Now()}, nil
}
@ -280,9 +280,9 @@ func (n *Node) Open(name string) (fs.File, error) {
return nil, core.E("node.Open", core.Concat("path not found: ", name), fs.ErrNotExist)
}
func (n *Node) Stat(name string) (fs.FileInfo, error) {
func (node *Node) Stat(name string) (fs.FileInfo, error) {
name = core.TrimPrefix(name, "/")
if file, ok := n.files[name]; ok {
if file, ok := node.files[name]; ok {
return file.Stat()
}
// Check if it's a directory
@ -290,7 +290,7 @@ func (n *Node) Stat(name string) (fs.FileInfo, error) {
if name == "." || name == "" {
prefix = ""
}
for filePath := range n.files {
for filePath := range node.files {
if core.HasPrefix(filePath, prefix) {
return &dirInfo{name: path.Base(name), modTime: time.Now()}, nil
}
@ -298,14 +298,14 @@ func (n *Node) Stat(name string) (fs.FileInfo, error) {
return nil, core.E("node.Stat", core.Concat("path not found: ", name), fs.ErrNotExist)
}
func (n *Node) ReadDir(name string) ([]fs.DirEntry, error) {
func (node *Node) ReadDir(name string) ([]fs.DirEntry, error) {
name = core.TrimPrefix(name, "/")
if name == "." {
name = ""
}
// Disallow reading a file as a directory.
if info, err := n.Stat(name); err == nil && !info.IsDir() {
if info, err := node.Stat(name); err == nil && !info.IsDir() {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
}
@ -317,7 +317,7 @@ func (n *Node) ReadDir(name string) ([]fs.DirEntry, error) {
prefix = name + "/"
}
for filePath := range n.files {
for filePath := range node.files {
if !core.HasPrefix(filePath, prefix) {
continue
}
@ -334,7 +334,7 @@ func (n *Node) ReadDir(name string) ([]fs.DirEntry, error) {
dir := &dirInfo{name: firstComponent, modTime: time.Now()}
entries = append(entries, fs.FileInfoToDirEntry(dir))
} else {
file := n.files[filePath]
file := node.files[filePath]
info, _ := file.Stat()
entries = append(entries, fs.FileInfoToDirEntry(info))
}
@ -349,52 +349,52 @@ func (n *Node) ReadDir(name string) ([]fs.DirEntry, error) {
// ---------- Medium interface: read/write ----------
func (n *Node) Read(filePath string) (string, error) {
func (node *Node) Read(filePath string) (string, error) {
filePath = core.TrimPrefix(filePath, "/")
f, ok := n.files[filePath]
f, ok := node.files[filePath]
if !ok {
return "", core.E("node.Read", core.Concat("path not found: ", filePath), fs.ErrNotExist)
}
return string(f.content), nil
}
func (n *Node) Write(filePath, content string) error {
n.AddData(filePath, []byte(content))
func (node *Node) Write(filePath, content string) error {
node.AddData(filePath, []byte(content))
return nil
}
func (n *Node) WriteMode(filePath, content string, mode fs.FileMode) error {
return n.Write(filePath, content)
func (node *Node) WriteMode(filePath, content string, mode fs.FileMode) error {
return node.Write(filePath, content)
}
func (n *Node) FileGet(filePath string) (string, error) {
return n.Read(filePath)
func (node *Node) FileGet(filePath string) (string, error) {
return node.Read(filePath)
}
func (n *Node) FileSet(filePath, content string) error {
return n.Write(filePath, content)
func (node *Node) FileSet(filePath, content string) error {
return node.Write(filePath, content)
}
// Example: _ = nodeTree.EnsureDir("config")
func (n *Node) EnsureDir(_ string) error {
func (node *Node) EnsureDir(_ string) error {
return nil
}
// ---------- Medium interface: existence checks ----------
func (n *Node) Exists(filePath string) bool {
_, err := n.Stat(filePath)
func (node *Node) Exists(filePath string) bool {
_, err := node.Stat(filePath)
return err == nil
}
func (n *Node) IsFile(filePath string) bool {
func (node *Node) IsFile(filePath string) bool {
filePath = core.TrimPrefix(filePath, "/")
_, ok := n.files[filePath]
_, ok := node.files[filePath]
return ok
}
func (n *Node) IsDir(filePath string) bool {
info, err := n.Stat(filePath)
func (node *Node) IsDir(filePath string) bool {
info, err := node.Stat(filePath)
if err != nil {
return false
}
@ -403,28 +403,28 @@ func (n *Node) IsDir(filePath string) bool {
// ---------- Medium interface: mutations ----------
func (n *Node) Delete(filePath string) error {
func (node *Node) Delete(filePath string) error {
filePath = core.TrimPrefix(filePath, "/")
if _, ok := n.files[filePath]; ok {
delete(n.files, filePath)
if _, ok := node.files[filePath]; ok {
delete(node.files, filePath)
return nil
}
return core.E("node.Delete", core.Concat("path not found: ", filePath), fs.ErrNotExist)
}
func (n *Node) DeleteAll(filePath string) error {
func (node *Node) DeleteAll(filePath string) error {
filePath = core.TrimPrefix(filePath, "/")
found := false
if _, ok := n.files[filePath]; ok {
delete(n.files, filePath)
if _, ok := node.files[filePath]; ok {
delete(node.files, filePath)
found = true
}
prefix := filePath + "/"
for entryPath := range n.files {
for entryPath := range node.files {
if core.HasPrefix(entryPath, prefix) {
delete(n.files, entryPath)
delete(node.files, entryPath)
found = true
}
}
@ -435,56 +435,56 @@ func (n *Node) DeleteAll(filePath string) error {
return nil
}
func (n *Node) Rename(oldPath, newPath string) error {
func (node *Node) Rename(oldPath, newPath string) error {
oldPath = core.TrimPrefix(oldPath, "/")
newPath = core.TrimPrefix(newPath, "/")
f, ok := n.files[oldPath]
f, ok := node.files[oldPath]
if !ok {
return core.E("node.Rename", core.Concat("path not found: ", oldPath), fs.ErrNotExist)
}
f.name = newPath
n.files[newPath] = f
delete(n.files, oldPath)
node.files[newPath] = f
delete(node.files, oldPath)
return nil
}
func (n *Node) List(filePath string) ([]fs.DirEntry, error) {
func (node *Node) List(filePath string) ([]fs.DirEntry, error) {
filePath = core.TrimPrefix(filePath, "/")
if filePath == "" || filePath == "." {
return n.ReadDir(".")
return node.ReadDir(".")
}
return n.ReadDir(filePath)
return node.ReadDir(filePath)
}
// ---------- Medium interface: streams ----------
func (n *Node) Create(filePath string) (goio.WriteCloser, error) {
func (node *Node) Create(filePath string) (goio.WriteCloser, error) {
filePath = core.TrimPrefix(filePath, "/")
return &nodeWriter{node: n, path: filePath}, nil
return &nodeWriter{node: node, path: filePath}, nil
}
func (n *Node) Append(filePath string) (goio.WriteCloser, error) {
func (node *Node) Append(filePath string) (goio.WriteCloser, error) {
filePath = core.TrimPrefix(filePath, "/")
var existing []byte
if f, ok := n.files[filePath]; ok {
if f, ok := node.files[filePath]; ok {
existing = make([]byte, len(f.content))
copy(existing, f.content)
}
return &nodeWriter{node: n, path: filePath, buf: existing}, nil
return &nodeWriter{node: node, path: filePath, buf: existing}, nil
}
func (n *Node) ReadStream(filePath string) (goio.ReadCloser, error) {
f, err := n.Open(filePath)
func (node *Node) ReadStream(filePath string) (goio.ReadCloser, error) {
f, err := node.Open(filePath)
if err != nil {
return nil, err
}
return goio.NopCloser(f), nil
}
func (n *Node) WriteStream(filePath string) (goio.WriteCloser, error) {
return n.Create(filePath)
func (node *Node) WriteStream(filePath string) (goio.WriteCloser, error) {
return node.Create(filePath)
}
// ---------- Internal types ----------

162
s3/s3.go
View file

@ -100,16 +100,16 @@ func New(options Options) (*Medium, error) {
if options.Client == nil {
return nil, core.E("s3.New", "client is required", nil)
}
m := &Medium{
medium := &Medium{
client: options.Client,
bucket: options.Bucket,
prefix: normalisePrefix(options.Prefix),
}
return m, nil
return medium, nil
}
// objectKey maps a virtual path to the full S3 object key.
func (m *Medium) objectKey(filePath string) string {
func (medium *Medium) objectKey(filePath string) string {
// Clean the path using a leading "/" to sandbox traversal attempts,
// then strip the "/" prefix. This ensures ".." can't escape.
clean := path.Clean("/" + filePath)
@ -118,23 +118,23 @@ func (m *Medium) objectKey(filePath string) string {
}
clean = core.TrimPrefix(clean, "/")
if m.prefix == "" {
if medium.prefix == "" {
return clean
}
if clean == "" {
return m.prefix
return medium.prefix
}
return m.prefix + clean
return medium.prefix + clean
}
func (m *Medium) Read(filePath string) (string, error) {
key := m.objectKey(filePath)
func (medium *Medium) Read(filePath string) (string, error) {
key := medium.objectKey(filePath)
if key == "" {
return "", core.E("s3.Read", "path is required", fs.ErrInvalid)
}
out, err := m.client.GetObject(context.Background(), &awss3.GetObjectInput{
Bucket: aws.String(m.bucket),
out, err := medium.client.GetObject(context.Background(), &awss3.GetObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
if err != nil {
@ -149,14 +149,14 @@ func (m *Medium) Read(filePath string) (string, error) {
return string(data), nil
}
func (m *Medium) Write(filePath, content string) error {
key := m.objectKey(filePath)
func (medium *Medium) Write(filePath, content string) error {
key := medium.objectKey(filePath)
if key == "" {
return core.E("s3.Write", "path is required", fs.ErrInvalid)
}
_, err := m.client.PutObject(context.Background(), &awss3.PutObjectInput{
Bucket: aws.String(m.bucket),
_, err := medium.client.PutObject(context.Background(), &awss3.PutObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
Body: core.NewReader(content),
})
@ -167,18 +167,18 @@ func (m *Medium) Write(filePath, content string) error {
}
// Example: _ = medium.WriteMode("keys/private.key", key, 0600)
func (m *Medium) WriteMode(filePath, content string, _ fs.FileMode) error {
return m.Write(filePath, content)
func (medium *Medium) WriteMode(filePath, content string, _ fs.FileMode) error {
return medium.Write(filePath, content)
}
// Example: _ = medium.EnsureDir("reports/2026")
func (m *Medium) EnsureDir(_ string) error {
func (medium *Medium) EnsureDir(_ string) error {
return nil
}
// Example: ok := medium.IsFile("reports/daily.txt")
func (m *Medium) IsFile(filePath string) bool {
key := m.objectKey(filePath)
func (medium *Medium) IsFile(filePath string) bool {
key := medium.objectKey(filePath)
if key == "" {
return false
}
@ -186,29 +186,29 @@ func (m *Medium) IsFile(filePath string) bool {
if core.HasSuffix(key, "/") {
return false
}
_, err := m.client.HeadObject(context.Background(), &awss3.HeadObjectInput{
Bucket: aws.String(m.bucket),
_, err := medium.client.HeadObject(context.Background(), &awss3.HeadObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
return err == nil
}
func (m *Medium) FileGet(filePath string) (string, error) {
return m.Read(filePath)
func (medium *Medium) FileGet(filePath string) (string, error) {
return medium.Read(filePath)
}
func (m *Medium) FileSet(filePath, content string) error {
return m.Write(filePath, content)
func (medium *Medium) FileSet(filePath, content string) error {
return medium.Write(filePath, content)
}
func (m *Medium) Delete(filePath string) error {
key := m.objectKey(filePath)
func (medium *Medium) Delete(filePath string) error {
key := medium.objectKey(filePath)
if key == "" {
return core.E("s3.Delete", "path is required", fs.ErrInvalid)
}
_, err := m.client.DeleteObject(context.Background(), &awss3.DeleteObjectInput{
Bucket: aws.String(m.bucket),
_, err := medium.client.DeleteObject(context.Background(), &awss3.DeleteObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
if err != nil {
@ -218,15 +218,15 @@ func (m *Medium) Delete(filePath string) error {
}
// Example: _ = medium.DeleteAll("reports/2026")
func (m *Medium) DeleteAll(filePath string) error {
key := m.objectKey(filePath)
func (medium *Medium) DeleteAll(filePath string) error {
key := medium.objectKey(filePath)
if key == "" {
return core.E("s3.DeleteAll", "path is required", fs.ErrInvalid)
}
// First, try deleting the exact key
_, err := m.client.DeleteObject(context.Background(), &awss3.DeleteObjectInput{
Bucket: aws.String(m.bucket),
_, err := medium.client.DeleteObject(context.Background(), &awss3.DeleteObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
if err != nil {
@ -243,8 +243,8 @@ func (m *Medium) DeleteAll(filePath string) error {
var continuationToken *string
for paginator {
listOut, err := m.client.ListObjectsV2(context.Background(), &awss3.ListObjectsV2Input{
Bucket: aws.String(m.bucket),
listOut, err := medium.client.ListObjectsV2(context.Background(), &awss3.ListObjectsV2Input{
Bucket: aws.String(medium.bucket),
Prefix: aws.String(prefix),
ContinuationToken: continuationToken,
})
@ -261,8 +261,8 @@ func (m *Medium) DeleteAll(filePath string) error {
objects[i] = types.ObjectIdentifier{Key: obj.Key}
}
deleteOut, err := m.client.DeleteObjects(context.Background(), &awss3.DeleteObjectsInput{
Bucket: aws.String(m.bucket),
deleteOut, err := medium.client.DeleteObjects(context.Background(), &awss3.DeleteObjectsInput{
Bucket: aws.String(medium.bucket),
Delete: &types.Delete{Objects: objects, Quiet: aws.Bool(true)},
})
if err != nil {
@ -283,17 +283,17 @@ func (m *Medium) DeleteAll(filePath string) error {
}
// Example: _ = medium.Rename("drafts/todo.txt", "archive/todo.txt")
func (m *Medium) Rename(oldPath, newPath string) error {
oldKey := m.objectKey(oldPath)
newKey := m.objectKey(newPath)
func (medium *Medium) Rename(oldPath, newPath string) error {
oldKey := medium.objectKey(oldPath)
newKey := medium.objectKey(newPath)
if oldKey == "" || newKey == "" {
return core.E("s3.Rename", "both old and new paths are required", fs.ErrInvalid)
}
copySource := m.bucket + "/" + oldKey
copySource := medium.bucket + "/" + oldKey
_, err := m.client.CopyObject(context.Background(), &awss3.CopyObjectInput{
Bucket: aws.String(m.bucket),
_, err := medium.client.CopyObject(context.Background(), &awss3.CopyObjectInput{
Bucket: aws.String(medium.bucket),
CopySource: aws.String(copySource),
Key: aws.String(newKey),
})
@ -301,8 +301,8 @@ func (m *Medium) Rename(oldPath, newPath string) error {
return core.E("s3.Rename", core.Concat("failed to copy object: ", oldKey, " -> ", newKey), err)
}
_, err = m.client.DeleteObject(context.Background(), &awss3.DeleteObjectInput{
Bucket: aws.String(m.bucket),
_, err = medium.client.DeleteObject(context.Background(), &awss3.DeleteObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(oldKey),
})
if err != nil {
@ -313,16 +313,16 @@ func (m *Medium) Rename(oldPath, newPath string) error {
}
// Example: entries, _ := medium.List("reports")
func (m *Medium) List(filePath string) ([]fs.DirEntry, error) {
prefix := m.objectKey(filePath)
func (medium *Medium) List(filePath string) ([]fs.DirEntry, error) {
prefix := medium.objectKey(filePath)
if prefix != "" && !core.HasSuffix(prefix, "/") {
prefix += "/"
}
var entries []fs.DirEntry
listOut, err := m.client.ListObjectsV2(context.Background(), &awss3.ListObjectsV2Input{
Bucket: aws.String(m.bucket),
listOut, err := medium.client.ListObjectsV2(context.Background(), &awss3.ListObjectsV2Input{
Bucket: aws.String(medium.bucket),
Prefix: aws.String(prefix),
Delimiter: aws.String("/"),
})
@ -386,14 +386,14 @@ func (m *Medium) List(filePath string) ([]fs.DirEntry, error) {
}
// Example: info, _ := medium.Stat("reports/daily.txt")
func (m *Medium) Stat(filePath string) (fs.FileInfo, error) {
key := m.objectKey(filePath)
func (medium *Medium) Stat(filePath string) (fs.FileInfo, error) {
key := medium.objectKey(filePath)
if key == "" {
return nil, core.E("s3.Stat", "path is required", fs.ErrInvalid)
}
out, err := m.client.HeadObject(context.Background(), &awss3.HeadObjectInput{
Bucket: aws.String(m.bucket),
out, err := medium.client.HeadObject(context.Background(), &awss3.HeadObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
if err != nil {
@ -418,14 +418,14 @@ func (m *Medium) Stat(filePath string) (fs.FileInfo, error) {
}, nil
}
func (m *Medium) Open(filePath string) (fs.File, error) {
key := m.objectKey(filePath)
func (medium *Medium) Open(filePath string) (fs.File, error) {
key := medium.objectKey(filePath)
if key == "" {
return nil, core.E("s3.Open", "path is required", fs.ErrInvalid)
}
out, err := m.client.GetObject(context.Background(), &awss3.GetObjectInput{
Bucket: aws.String(m.bucket),
out, err := medium.client.GetObject(context.Background(), &awss3.GetObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
if err != nil {
@ -456,27 +456,27 @@ func (m *Medium) Open(filePath string) (fs.File, error) {
}
// Example: writer, _ := medium.Create("reports/daily.txt")
func (m *Medium) Create(filePath string) (goio.WriteCloser, error) {
key := m.objectKey(filePath)
func (medium *Medium) Create(filePath string) (goio.WriteCloser, error) {
key := medium.objectKey(filePath)
if key == "" {
return nil, core.E("s3.Create", "path is required", fs.ErrInvalid)
}
return &s3WriteCloser{
medium: m,
medium: medium,
key: key,
}, nil
}
// Example: writer, _ := medium.Append("reports/daily.txt")
func (m *Medium) Append(filePath string) (goio.WriteCloser, error) {
key := m.objectKey(filePath)
func (medium *Medium) Append(filePath string) (goio.WriteCloser, error) {
key := medium.objectKey(filePath)
if key == "" {
return nil, core.E("s3.Append", "path is required", fs.ErrInvalid)
}
var existing []byte
out, err := m.client.GetObject(context.Background(), &awss3.GetObjectInput{
Bucket: aws.String(m.bucket),
out, err := medium.client.GetObject(context.Background(), &awss3.GetObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
if err == nil {
@ -485,20 +485,20 @@ func (m *Medium) Append(filePath string) (goio.WriteCloser, error) {
}
return &s3WriteCloser{
medium: m,
medium: medium,
key: key,
data: existing,
}, nil
}
func (m *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
key := m.objectKey(filePath)
func (medium *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
key := medium.objectKey(filePath)
if key == "" {
return nil, core.E("s3.ReadStream", "path is required", fs.ErrInvalid)
}
out, err := m.client.GetObject(context.Background(), &awss3.GetObjectInput{
Bucket: aws.String(m.bucket),
out, err := medium.client.GetObject(context.Background(), &awss3.GetObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
if err != nil {
@ -507,20 +507,20 @@ func (m *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
return out.Body, nil
}
func (m *Medium) WriteStream(filePath string) (goio.WriteCloser, error) {
return m.Create(filePath)
func (medium *Medium) WriteStream(filePath string) (goio.WriteCloser, error) {
return medium.Create(filePath)
}
// Example: ok := medium.Exists("reports/daily.txt")
func (m *Medium) Exists(filePath string) bool {
key := m.objectKey(filePath)
func (medium *Medium) Exists(filePath string) bool {
key := medium.objectKey(filePath)
if key == "" {
return false
}
// Check as an exact object
_, err := m.client.HeadObject(context.Background(), &awss3.HeadObjectInput{
Bucket: aws.String(m.bucket),
_, err := medium.client.HeadObject(context.Background(), &awss3.HeadObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
})
if err == nil {
@ -532,8 +532,8 @@ func (m *Medium) Exists(filePath string) bool {
if !core.HasSuffix(prefix, "/") {
prefix += "/"
}
listOut, err := m.client.ListObjectsV2(context.Background(), &awss3.ListObjectsV2Input{
Bucket: aws.String(m.bucket),
listOut, err := medium.client.ListObjectsV2(context.Background(), &awss3.ListObjectsV2Input{
Bucket: aws.String(medium.bucket),
Prefix: aws.String(prefix),
MaxKeys: aws.Int32(1),
})
@ -544,8 +544,8 @@ func (m *Medium) Exists(filePath string) bool {
}
// Example: ok := medium.IsDir("reports")
func (m *Medium) IsDir(filePath string) bool {
key := m.objectKey(filePath)
func (medium *Medium) IsDir(filePath string) bool {
key := medium.objectKey(filePath)
if key == "" {
return false
}
@ -555,8 +555,8 @@ func (m *Medium) IsDir(filePath string) bool {
prefix += "/"
}
listOut, err := m.client.ListObjectsV2(context.Background(), &awss3.ListObjectsV2Input{
Bucket: aws.String(m.bucket),
listOut, err := medium.client.ListObjectsV2(context.Background(), &awss3.ListObjectsV2Input{
Bucket: aws.String(medium.bucket),
Prefix: aws.String(prefix),
MaxKeys: aws.Int32(1),
})

View file

@ -45,25 +45,25 @@ type PreObfuscator interface {
type XORObfuscator struct{}
// Obfuscate XORs the data with a key stream derived from the entropy.
func (x *XORObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
func (obfuscator *XORObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
if len(data) == 0 {
return data
}
return x.transform(data, entropy)
return obfuscator.transform(data, entropy)
}
// Deobfuscate reverses the XOR transformation (XOR is symmetric).
func (x *XORObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
func (obfuscator *XORObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
if len(data) == 0 {
return data
}
return x.transform(data, entropy)
return obfuscator.transform(data, entropy)
}
// transform applies XOR with an entropy-derived key stream.
func (x *XORObfuscator) transform(data []byte, entropy []byte) []byte {
func (obfuscator *XORObfuscator) transform(data []byte, entropy []byte) []byte {
result := make([]byte, len(data))
keyStream := x.deriveKeyStream(entropy, len(data))
keyStream := obfuscator.deriveKeyStream(entropy, len(data))
for i := range data {
result[i] = data[i] ^ keyStream[i]
}
@ -71,7 +71,7 @@ func (x *XORObfuscator) transform(data []byte, entropy []byte) []byte {
}
// deriveKeyStream creates a deterministic key stream from entropy.
func (x *XORObfuscator) deriveKeyStream(entropy []byte, length int) []byte {
func (obfuscator *XORObfuscator) deriveKeyStream(entropy []byte, length int) []byte {
stream := make([]byte, length)
h := sha256.New()
@ -98,7 +98,7 @@ func (x *XORObfuscator) deriveKeyStream(entropy []byte, length int) []byte {
type ShuffleMaskObfuscator struct{}
// Obfuscate shuffles bytes and applies a mask derived from entropy.
func (s *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
func (obfuscator *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
if len(data) == 0 {
return data
}
@ -107,8 +107,8 @@ func (s *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
copy(result, data)
// Generate permutation and mask from entropy
perm := s.generatePermutation(entropy, len(data))
mask := s.deriveMask(entropy, len(data))
perm := obfuscator.generatePermutation(entropy, len(data))
mask := obfuscator.deriveMask(entropy, len(data))
// Apply mask first, then shuffle
for i := range result {
@ -125,7 +125,7 @@ func (s *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
}
// Deobfuscate reverses the shuffle and mask operations.
func (s *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
func (obfuscator *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
if len(data) == 0 {
return data
}
@ -133,8 +133,8 @@ func (s *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte) []byte
result := make([]byte, len(data))
// Generate permutation and mask from entropy
perm := s.generatePermutation(entropy, len(data))
mask := s.deriveMask(entropy, len(data))
perm := obfuscator.generatePermutation(entropy, len(data))
mask := obfuscator.deriveMask(entropy, len(data))
// Unshuffle first
for i, p := range perm {
@ -150,7 +150,7 @@ func (s *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte) []byte
}
// generatePermutation creates a deterministic permutation from entropy.
func (s *ShuffleMaskObfuscator) generatePermutation(entropy []byte, length int) []int {
func (obfuscator *ShuffleMaskObfuscator) generatePermutation(entropy []byte, length int) []int {
perm := make([]int, length)
for i := range perm {
perm[i] = i
@ -178,7 +178,7 @@ func (s *ShuffleMaskObfuscator) generatePermutation(entropy []byte, length int)
}
// deriveMask creates a mask byte array from entropy.
func (s *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int) []byte {
func (obfuscator *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int) []byte {
mask := make([]byte, length)
h := sha256.New()
@ -247,22 +247,22 @@ func NewChaChaPolySigilWithObfuscator(key []byte, obfuscator PreObfuscator) (*Ch
}
// In encrypts plaintext with the configured pre-obfuscator.
func (s *ChaChaPolySigil) In(data []byte) ([]byte, error) {
if s.Key == nil {
func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
if sigil.Key == nil {
return nil, NoKeyConfiguredError
}
if data == nil {
return nil, nil
}
aead, err := chacha20poly1305.NewX(s.Key)
aead, err := chacha20poly1305.NewX(sigil.Key)
if err != nil {
return nil, core.E("sigil.ChaChaPolySigil.In", "create cipher", err)
}
// Generate nonce
nonce := make([]byte, aead.NonceSize())
reader := s.randomReader
reader := sigil.randomReader
if reader == nil {
reader = rand.Reader
}
@ -273,8 +273,8 @@ func (s *ChaChaPolySigil) In(data []byte) ([]byte, error) {
// Pre-obfuscate the plaintext using nonce as entropy
// This ensures CPU encryption routines never see raw plaintext
obfuscated := data
if s.Obfuscator != nil {
obfuscated = s.Obfuscator.Obfuscate(data, nonce)
if sigil.Obfuscator != nil {
obfuscated = sigil.Obfuscator.Obfuscate(data, nonce)
}
// Encrypt the obfuscated data
@ -285,15 +285,15 @@ func (s *ChaChaPolySigil) In(data []byte) ([]byte, error) {
}
// Out decrypts ciphertext and reverses the pre-obfuscation step.
func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
if s.Key == nil {
func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
if sigil.Key == nil {
return nil, NoKeyConfiguredError
}
if data == nil {
return nil, nil
}
aead, err := chacha20poly1305.NewX(s.Key)
aead, err := chacha20poly1305.NewX(sigil.Key)
if err != nil {
return nil, core.E("sigil.ChaChaPolySigil.Out", "create cipher", err)
}
@ -315,8 +315,8 @@ func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
// Deobfuscate using the same nonce as entropy
plaintext := obfuscated
if s.Obfuscator != nil {
plaintext = s.Obfuscator.Deobfuscate(obfuscated, nonce)
if sigil.Obfuscator != nil {
plaintext = sigil.Obfuscator.Deobfuscate(obfuscated, nonce)
}
if len(plaintext) == 0 {

View file

@ -20,8 +20,8 @@ type Sigil interface {
// Example: encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
func Transmute(data []byte, sigils []Sigil) ([]byte, error) {
var err error
for _, s := range sigils {
data, err = s.In(data)
for _, sigilValue := range sigils {
data, err = sigilValue.In(data)
if err != nil {
return nil, core.E("sigil.Transmute", "sigil in failed", err)
}

View file

@ -25,7 +25,7 @@ import (
type ReverseSigil struct{}
// In reverses the bytes of the data.
func (s *ReverseSigil) In(data []byte) ([]byte, error) {
func (sigil *ReverseSigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
}
@ -37,8 +37,8 @@ func (s *ReverseSigil) In(data []byte) ([]byte, error) {
}
// Out reverses the bytes of the data.
func (s *ReverseSigil) Out(data []byte) ([]byte, error) {
return s.In(data)
func (sigil *ReverseSigil) Out(data []byte) ([]byte, error) {
return sigil.In(data)
}
// HexSigil is a Sigil that encodes/decodes data to/from hexadecimal.
@ -46,7 +46,7 @@ func (s *ReverseSigil) Out(data []byte) ([]byte, error) {
type HexSigil struct{}
// In encodes the data to hexadecimal.
func (s *HexSigil) In(data []byte) ([]byte, error) {
func (sigil *HexSigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
}
@ -56,7 +56,7 @@ func (s *HexSigil) In(data []byte) ([]byte, error) {
}
// Out decodes the data from hexadecimal.
func (s *HexSigil) Out(data []byte) ([]byte, error) {
func (sigil *HexSigil) Out(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
}
@ -70,7 +70,7 @@ func (s *HexSigil) Out(data []byte) ([]byte, error) {
type Base64Sigil struct{}
// In encodes the data to base64.
func (s *Base64Sigil) In(data []byte) ([]byte, error) {
func (sigil *Base64Sigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
}
@ -80,7 +80,7 @@ func (s *Base64Sigil) In(data []byte) ([]byte, error) {
}
// Out decodes the data from base64.
func (s *Base64Sigil) Out(data []byte) ([]byte, error) {
func (sigil *Base64Sigil) Out(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
}
@ -96,12 +96,12 @@ type GzipSigil struct {
}
// In compresses the data using gzip.
func (s *GzipSigil) In(data []byte) ([]byte, error) {
func (sigil *GzipSigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
}
var b bytes.Buffer
outputWriter := s.outputWriter
outputWriter := sigil.outputWriter
if outputWriter == nil {
outputWriter = &b
}
@ -116,7 +116,7 @@ func (s *GzipSigil) In(data []byte) ([]byte, error) {
}
// Out decompresses the data using gzip.
func (s *GzipSigil) Out(data []byte) ([]byte, error) {
func (sigil *GzipSigil) Out(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
}
@ -137,7 +137,7 @@ func (s *GzipSigil) Out(data []byte) ([]byte, error) {
type JSONSigil struct{ Indent bool }
// In compacts or indents the JSON data.
func (s *JSONSigil) In(data []byte) ([]byte, error) {
func (sigil *JSONSigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
}
@ -152,14 +152,14 @@ func (s *JSONSigil) In(data []byte) ([]byte, error) {
}
compact := core.JSONMarshalString(decoded)
if s.Indent {
if sigil.Indent {
return []byte(indentJSON(compact)), nil
}
return []byte(compact), nil
}
// Out is a no-op for JSONSigil.
func (s *JSONSigil) Out(data []byte) ([]byte, error) {
func (sigil *JSONSigil) Out(data []byte) ([]byte, error) {
// For simplicity, Out is a no-op. The primary use is formatting.
return data, nil
}
@ -179,9 +179,9 @@ func NewHashSigil(h crypto.Hash) *HashSigil {
}
// In hashes the data.
func (s *HashSigil) In(data []byte) ([]byte, error) {
func (sigil *HashSigil) In(data []byte) ([]byte, error) {
var hasher goio.Writer
switch s.Hash {
switch sigil.Hash {
case crypto.MD4:
hasher = md4.New()
case crypto.MD5:
@ -228,7 +228,7 @@ func (s *HashSigil) In(data []byte) ([]byte, error) {
}
// Out is a no-op for HashSigil.
func (s *HashSigil) Out(data []byte) ([]byte, error) {
func (sigil *HashSigil) Out(data []byte) ([]byte, error) {
return data, nil
}

View file

@ -79,9 +79,9 @@ func New(options Options) (*Medium, error) {
}
// Close closes the underlying database connection.
func (m *Medium) Close() error {
if m.database != nil {
return m.database.Close()
func (medium *Medium) Close() error {
if medium.database != nil {
return medium.database.Close()
}
return nil
}
@ -96,7 +96,7 @@ func normaliseEntryPath(filePath string) string {
return core.TrimPrefix(clean, "/")
}
func (m *Medium) Read(filePath string) (string, error) {
func (medium *Medium) Read(filePath string) (string, error) {
key := normaliseEntryPath(filePath)
if key == "" {
return "", core.E("sqlite.Read", "path is required", fs.ErrInvalid)
@ -104,8 +104,8 @@ func (m *Medium) Read(filePath string) (string, error) {
var content []byte
var isDir bool
err := m.database.QueryRow(
`SELECT content, is_dir FROM `+m.table+` WHERE path = ?`, key,
err := medium.database.QueryRow(
`SELECT content, is_dir FROM `+medium.table+` WHERE path = ?`, key,
).Scan(&content, &isDir)
if err == sql.ErrNoRows {
return "", core.E("sqlite.Read", core.Concat("file not found: ", key), fs.ErrNotExist)
@ -119,19 +119,19 @@ func (m *Medium) Read(filePath string) (string, error) {
return string(content), nil
}
func (m *Medium) Write(filePath, content string) error {
return m.WriteMode(filePath, content, 0644)
func (medium *Medium) Write(filePath, content string) error {
return medium.WriteMode(filePath, content, 0644)
}
// Example: _ = medium.WriteMode("keys/private.key", key, 0600)
func (m *Medium) WriteMode(filePath, content string, mode fs.FileMode) error {
func (medium *Medium) WriteMode(filePath, content string, mode fs.FileMode) error {
key := normaliseEntryPath(filePath)
if key == "" {
return core.E("sqlite.WriteMode", "path is required", fs.ErrInvalid)
}
_, err := m.database.Exec(
`INSERT INTO `+m.table+` (path, content, mode, is_dir, mtime) VALUES (?, ?, ?, FALSE, ?)
_, err := medium.database.Exec(
`INSERT INTO `+medium.table+` (path, content, mode, is_dir, mtime) VALUES (?, ?, ?, FALSE, ?)
ON CONFLICT(path) DO UPDATE SET content = excluded.content, mode = excluded.mode, is_dir = FALSE, mtime = excluded.mtime`,
key, []byte(content), int(mode), time.Now().UTC(),
)
@ -142,15 +142,15 @@ func (m *Medium) WriteMode(filePath, content string, mode fs.FileMode) error {
}
// Example: _ = medium.EnsureDir("config")
func (m *Medium) EnsureDir(filePath string) error {
func (medium *Medium) EnsureDir(filePath string) error {
key := normaliseEntryPath(filePath)
if key == "" {
// Root always "exists"
return nil
}
_, err := m.database.Exec(
`INSERT INTO `+m.table+` (path, content, mode, is_dir, mtime) VALUES (?, '', 493, TRUE, ?)
_, err := medium.database.Exec(
`INSERT INTO `+medium.table+` (path, content, mode, is_dir, mtime) VALUES (?, '', 493, TRUE, ?)
ON CONFLICT(path) DO NOTHING`,
key, time.Now().UTC(),
)
@ -160,15 +160,15 @@ func (m *Medium) EnsureDir(filePath string) error {
return nil
}
func (m *Medium) IsFile(filePath string) bool {
func (medium *Medium) IsFile(filePath string) bool {
key := normaliseEntryPath(filePath)
if key == "" {
return false
}
var isDir bool
err := m.database.QueryRow(
`SELECT is_dir FROM `+m.table+` WHERE path = ?`, key,
err := medium.database.QueryRow(
`SELECT is_dir FROM `+medium.table+` WHERE path = ?`, key,
).Scan(&isDir)
if err != nil {
return false
@ -176,16 +176,16 @@ func (m *Medium) IsFile(filePath string) bool {
return !isDir
}
func (m *Medium) FileGet(filePath string) (string, error) {
return m.Read(filePath)
func (medium *Medium) FileGet(filePath string) (string, error) {
return medium.Read(filePath)
}
func (m *Medium) FileSet(filePath, content string) error {
return m.Write(filePath, content)
func (medium *Medium) FileSet(filePath, content string) error {
return medium.Write(filePath, content)
}
// Example: _ = medium.Delete("config/app.yaml")
func (m *Medium) Delete(filePath string) error {
func (medium *Medium) Delete(filePath string) error {
key := normaliseEntryPath(filePath)
if key == "" {
return core.E("sqlite.Delete", "path is required", fs.ErrInvalid)
@ -193,8 +193,8 @@ func (m *Medium) Delete(filePath string) error {
// Check if it's a directory with children
var isDir bool
err := m.database.QueryRow(
`SELECT is_dir FROM `+m.table+` WHERE path = ?`, key,
err := medium.database.QueryRow(
`SELECT is_dir FROM `+medium.table+` WHERE path = ?`, key,
).Scan(&isDir)
if err == sql.ErrNoRows {
return core.E("sqlite.Delete", core.Concat("path not found: ", key), fs.ErrNotExist)
@ -207,8 +207,8 @@ func (m *Medium) Delete(filePath string) error {
// Check for children
prefix := key + "/"
var count int
err := m.database.QueryRow(
`SELECT COUNT(*) FROM `+m.table+` WHERE path LIKE ? AND path != ?`, prefix+"%", key,
err := medium.database.QueryRow(
`SELECT COUNT(*) FROM `+medium.table+` WHERE path LIKE ? AND path != ?`, prefix+"%", key,
).Scan(&count)
if err != nil {
return core.E("sqlite.Delete", core.Concat("count failed: ", key), err)
@ -218,7 +218,7 @@ func (m *Medium) Delete(filePath string) error {
}
}
res, err := m.database.Exec(`DELETE FROM `+m.table+` WHERE path = ?`, key)
res, err := medium.database.Exec(`DELETE FROM `+medium.table+` WHERE path = ?`, key)
if err != nil {
return core.E("sqlite.Delete", core.Concat("delete failed: ", key), err)
}
@ -230,7 +230,7 @@ func (m *Medium) Delete(filePath string) error {
}
// Example: _ = medium.DeleteAll("config")
func (m *Medium) DeleteAll(filePath string) error {
func (medium *Medium) DeleteAll(filePath string) error {
key := normaliseEntryPath(filePath)
if key == "" {
return core.E("sqlite.DeleteAll", "path is required", fs.ErrInvalid)
@ -239,8 +239,8 @@ func (m *Medium) DeleteAll(filePath string) error {
prefix := key + "/"
// Delete the exact path and all children
res, err := m.database.Exec(
`DELETE FROM `+m.table+` WHERE path = ? OR path LIKE ?`,
res, err := medium.database.Exec(
`DELETE FROM `+medium.table+` WHERE path = ? OR path LIKE ?`,
key, prefix+"%",
)
if err != nil {
@ -254,14 +254,14 @@ func (m *Medium) DeleteAll(filePath string) error {
}
// Example: _ = medium.Rename("drafts/todo.txt", "archive/todo.txt")
func (m *Medium) Rename(oldPath, newPath string) error {
func (medium *Medium) Rename(oldPath, newPath string) error {
oldKey := normaliseEntryPath(oldPath)
newKey := normaliseEntryPath(newPath)
if oldKey == "" || newKey == "" {
return core.E("sqlite.Rename", "both old and new paths are required", fs.ErrInvalid)
}
tx, err := m.database.Begin()
tx, err := medium.database.Begin()
if err != nil {
return core.E("sqlite.Rename", "begin tx failed", err)
}
@ -273,7 +273,7 @@ func (m *Medium) Rename(oldPath, newPath string) error {
var isDir bool
var mtime time.Time
err = tx.QueryRow(
`SELECT content, mode, is_dir, mtime FROM `+m.table+` WHERE path = ?`, oldKey,
`SELECT content, mode, is_dir, mtime FROM `+medium.table+` WHERE path = ?`, oldKey,
).Scan(&content, &mode, &isDir, &mtime)
if err == sql.ErrNoRows {
return core.E("sqlite.Rename", core.Concat("source not found: ", oldKey), fs.ErrNotExist)
@ -284,7 +284,7 @@ func (m *Medium) Rename(oldPath, newPath string) error {
// Insert or replace at new path
_, err = tx.Exec(
`INSERT INTO `+m.table+` (path, content, mode, is_dir, mtime) VALUES (?, ?, ?, ?, ?)
`INSERT INTO `+medium.table+` (path, content, mode, is_dir, mtime) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(path) DO UPDATE SET content = excluded.content, mode = excluded.mode, is_dir = excluded.is_dir, mtime = excluded.mtime`,
newKey, content, mode, isDir, mtime,
)
@ -293,7 +293,7 @@ func (m *Medium) Rename(oldPath, newPath string) error {
}
// Delete old path
_, err = tx.Exec(`DELETE FROM `+m.table+` WHERE path = ?`, oldKey)
_, err = tx.Exec(`DELETE FROM `+medium.table+` WHERE path = ?`, oldKey)
if err != nil {
return core.E("sqlite.Rename", core.Concat("delete old path failed: ", oldKey), err)
}
@ -304,7 +304,7 @@ func (m *Medium) Rename(oldPath, newPath string) error {
newPrefix := newKey + "/"
rows, err := tx.Query(
`SELECT path, content, mode, is_dir, mtime FROM `+m.table+` WHERE path LIKE ?`,
`SELECT path, content, mode, is_dir, mtime FROM `+medium.table+` WHERE path LIKE ?`,
oldPrefix+"%",
)
if err != nil {
@ -332,7 +332,7 @@ func (m *Medium) Rename(oldPath, newPath string) error {
for _, c := range children {
newChildPath := core.Concat(newPrefix, core.TrimPrefix(c.path, oldPrefix))
_, err = tx.Exec(
`INSERT INTO `+m.table+` (path, content, mode, is_dir, mtime) VALUES (?, ?, ?, ?, ?)
`INSERT INTO `+medium.table+` (path, content, mode, is_dir, mtime) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(path) DO UPDATE SET content = excluded.content, mode = excluded.mode, is_dir = excluded.is_dir, mtime = excluded.mtime`,
newChildPath, c.content, c.mode, c.isDir, c.mtime,
)
@ -342,7 +342,7 @@ func (m *Medium) Rename(oldPath, newPath string) error {
}
// Delete old children
_, err = tx.Exec(`DELETE FROM `+m.table+` WHERE path LIKE ?`, oldPrefix+"%")
_, err = tx.Exec(`DELETE FROM `+medium.table+` WHERE path LIKE ?`, oldPrefix+"%")
if err != nil {
return core.E("sqlite.Rename", "delete old children failed", err)
}
@ -352,15 +352,15 @@ func (m *Medium) Rename(oldPath, newPath string) error {
}
// Example: entries, _ := medium.List("config")
func (m *Medium) List(filePath string) ([]fs.DirEntry, error) {
func (medium *Medium) List(filePath string) ([]fs.DirEntry, error) {
prefix := normaliseEntryPath(filePath)
if prefix != "" {
prefix += "/"
}
// Query all paths under the prefix
rows, err := m.database.Query(
`SELECT path, content, mode, is_dir, mtime FROM `+m.table+` WHERE path LIKE ? OR path LIKE ?`,
rows, err := medium.database.Query(
`SELECT path, content, mode, is_dir, mtime FROM `+medium.table+` WHERE path LIKE ? OR path LIKE ?`,
prefix+"%", prefix+"%",
)
if err != nil {
@ -427,7 +427,7 @@ func (m *Medium) List(filePath string) ([]fs.DirEntry, error) {
return entries, rows.Err()
}
func (m *Medium) Stat(filePath string) (fs.FileInfo, error) {
func (medium *Medium) Stat(filePath string) (fs.FileInfo, error) {
key := normaliseEntryPath(filePath)
if key == "" {
return nil, core.E("sqlite.Stat", "path is required", fs.ErrInvalid)
@ -437,8 +437,8 @@ func (m *Medium) Stat(filePath string) (fs.FileInfo, error) {
var mode int
var isDir bool
var mtime time.Time
err := m.database.QueryRow(
`SELECT content, mode, is_dir, mtime FROM `+m.table+` WHERE path = ?`, key,
err := medium.database.QueryRow(
`SELECT content, mode, is_dir, mtime FROM `+medium.table+` WHERE path = ?`, key,
).Scan(&content, &mode, &isDir, &mtime)
if err == sql.ErrNoRows {
return nil, core.E("sqlite.Stat", core.Concat("path not found: ", key), fs.ErrNotExist)
@ -457,7 +457,7 @@ func (m *Medium) Stat(filePath string) (fs.FileInfo, error) {
}, nil
}
func (m *Medium) Open(filePath string) (fs.File, error) {
func (medium *Medium) Open(filePath string) (fs.File, error) {
key := normaliseEntryPath(filePath)
if key == "" {
return nil, core.E("sqlite.Open", "path is required", fs.ErrInvalid)
@ -467,8 +467,8 @@ func (m *Medium) Open(filePath string) (fs.File, error) {
var mode int
var isDir bool
var mtime time.Time
err := m.database.QueryRow(
`SELECT content, mode, is_dir, mtime FROM `+m.table+` WHERE path = ?`, key,
err := medium.database.QueryRow(
`SELECT content, mode, is_dir, mtime FROM `+medium.table+` WHERE path = ?`, key,
).Scan(&content, &mode, &isDir, &mtime)
if err == sql.ErrNoRows {
return nil, core.E("sqlite.Open", core.Concat("file not found: ", key), fs.ErrNotExist)
@ -488,39 +488,39 @@ func (m *Medium) Open(filePath string) (fs.File, error) {
}, nil
}
func (m *Medium) Create(filePath string) (goio.WriteCloser, error) {
func (medium *Medium) Create(filePath string) (goio.WriteCloser, error) {
key := normaliseEntryPath(filePath)
if key == "" {
return nil, core.E("sqlite.Create", "path is required", fs.ErrInvalid)
}
return &sqliteWriteCloser{
medium: m,
medium: medium,
path: key,
}, nil
}
func (m *Medium) Append(filePath string) (goio.WriteCloser, error) {
func (medium *Medium) Append(filePath string) (goio.WriteCloser, error) {
key := normaliseEntryPath(filePath)
if key == "" {
return nil, core.E("sqlite.Append", "path is required", fs.ErrInvalid)
}
var existing []byte
err := m.database.QueryRow(
`SELECT content FROM `+m.table+` WHERE path = ? AND is_dir = FALSE`, key,
err := medium.database.QueryRow(
`SELECT content FROM `+medium.table+` WHERE path = ? AND is_dir = FALSE`, key,
).Scan(&existing)
if err != nil && err != sql.ErrNoRows {
return nil, core.E("sqlite.Append", core.Concat("query failed: ", key), err)
}
return &sqliteWriteCloser{
medium: m,
medium: medium,
path: key,
data: existing,
}, nil
}
func (m *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
func (medium *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
key := normaliseEntryPath(filePath)
if key == "" {
return nil, core.E("sqlite.ReadStream", "path is required", fs.ErrInvalid)
@ -528,8 +528,8 @@ func (m *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
var content []byte
var isDir bool
err := m.database.QueryRow(
`SELECT content, is_dir FROM `+m.table+` WHERE path = ?`, key,
err := medium.database.QueryRow(
`SELECT content, is_dir FROM `+medium.table+` WHERE path = ?`, key,
).Scan(&content, &isDir)
if err == sql.ErrNoRows {
return nil, core.E("sqlite.ReadStream", core.Concat("file not found: ", key), fs.ErrNotExist)
@ -544,11 +544,11 @@ func (m *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
return goio.NopCloser(bytes.NewReader(content)), nil
}
func (m *Medium) WriteStream(filePath string) (goio.WriteCloser, error) {
return m.Create(filePath)
func (medium *Medium) WriteStream(filePath string) (goio.WriteCloser, error) {
return medium.Create(filePath)
}
func (m *Medium) Exists(filePath string) bool {
func (medium *Medium) Exists(filePath string) bool {
key := normaliseEntryPath(filePath)
if key == "" {
// Root always exists
@ -556,8 +556,8 @@ func (m *Medium) Exists(filePath string) bool {
}
var count int
err := m.database.QueryRow(
`SELECT COUNT(*) FROM `+m.table+` WHERE path = ?`, key,
err := medium.database.QueryRow(
`SELECT COUNT(*) FROM `+medium.table+` WHERE path = ?`, key,
).Scan(&count)
if err != nil {
return false
@ -565,15 +565,15 @@ func (m *Medium) Exists(filePath string) bool {
return count > 0
}
func (m *Medium) IsDir(filePath string) bool {
func (medium *Medium) IsDir(filePath string) bool {
key := normaliseEntryPath(filePath)
if key == "" {
return false
}
var isDir bool
err := m.database.QueryRow(
`SELECT is_dir FROM `+m.table+` WHERE path = ?`, key,
err := medium.database.QueryRow(
`SELECT is_dir FROM `+medium.table+` WHERE path = ?`, key,
).Scan(&isDir)
if err != nil {
return false

View file

@ -31,18 +31,18 @@ func NewMedium(options Options) (*Medium, error) {
}
// Example: medium := keyValueStore.AsMedium()
func (s *Store) AsMedium() *Medium {
return &Medium{store: s}
func (store *Store) AsMedium() *Medium {
return &Medium{store: store}
}
// Example: keyValueStore := medium.Store()
func (m *Medium) Store() *Store {
return m.store
func (medium *Medium) Store() *Store {
return medium.store
}
// Example: _ = medium.Close()
func (m *Medium) Close() error {
return m.store.Close()
func (medium *Medium) Close() error {
return medium.store.Close()
}
// splitGroupKeyPath splits a group/key path into store components.
@ -59,56 +59,56 @@ func splitGroupKeyPath(entryPath string) (group, key string) {
return parts[0], parts[1]
}
func (m *Medium) Read(entryPath string) (string, error) {
func (medium *Medium) Read(entryPath string) (string, error) {
group, key := splitGroupKeyPath(entryPath)
if key == "" {
return "", core.E("store.Read", "path must include group/key", fs.ErrInvalid)
}
return m.store.Get(group, key)
return medium.store.Get(group, key)
}
func (m *Medium) Write(entryPath, content string) error {
func (medium *Medium) Write(entryPath, content string) error {
group, key := splitGroupKeyPath(entryPath)
if key == "" {
return core.E("store.Write", "path must include group/key", fs.ErrInvalid)
}
return m.store.Set(group, key, content)
return medium.store.Set(group, key, content)
}
// Example: _ = medium.WriteMode("app/theme", "midnight", 0600)
func (m *Medium) WriteMode(entryPath, content string, _ fs.FileMode) error {
return m.Write(entryPath, content)
func (medium *Medium) WriteMode(entryPath, content string, _ fs.FileMode) error {
return medium.Write(entryPath, content)
}
// Example: _ = medium.EnsureDir("app")
func (m *Medium) EnsureDir(_ string) error {
func (medium *Medium) EnsureDir(_ string) error {
return nil
}
func (m *Medium) IsFile(entryPath string) bool {
func (medium *Medium) IsFile(entryPath string) bool {
group, key := splitGroupKeyPath(entryPath)
if key == "" {
return false
}
_, err := m.store.Get(group, key)
_, err := medium.store.Get(group, key)
return err == nil
}
func (m *Medium) FileGet(entryPath string) (string, error) {
return m.Read(entryPath)
func (medium *Medium) FileGet(entryPath string) (string, error) {
return medium.Read(entryPath)
}
func (m *Medium) FileSet(entryPath, content string) error {
return m.Write(entryPath, content)
func (medium *Medium) FileSet(entryPath, content string) error {
return medium.Write(entryPath, content)
}
func (m *Medium) Delete(entryPath string) error {
func (medium *Medium) Delete(entryPath string) error {
group, key := splitGroupKeyPath(entryPath)
if group == "" {
return core.E("store.Delete", "path is required", fs.ErrInvalid)
}
if key == "" {
entryCount, err := m.store.Count(group)
entryCount, err := medium.store.Count(group)
if err != nil {
return err
}
@ -117,42 +117,42 @@ func (m *Medium) Delete(entryPath string) error {
}
return nil
}
return m.store.Delete(group, key)
return medium.store.Delete(group, key)
}
func (m *Medium) DeleteAll(entryPath string) error {
func (medium *Medium) DeleteAll(entryPath string) error {
group, key := splitGroupKeyPath(entryPath)
if group == "" {
return core.E("store.DeleteAll", "path is required", fs.ErrInvalid)
}
if key == "" {
return m.store.DeleteGroup(group)
return medium.store.DeleteGroup(group)
}
return m.store.Delete(group, key)
return medium.store.Delete(group, key)
}
func (m *Medium) Rename(oldPath, newPath string) error {
func (medium *Medium) Rename(oldPath, newPath string) error {
oldGroup, oldKey := splitGroupKeyPath(oldPath)
newGroup, newKey := splitGroupKeyPath(newPath)
if oldKey == "" || newKey == "" {
return core.E("store.Rename", "both paths must include group/key", fs.ErrInvalid)
}
val, err := m.store.Get(oldGroup, oldKey)
val, err := medium.store.Get(oldGroup, oldKey)
if err != nil {
return err
}
if err := m.store.Set(newGroup, newKey, val); err != nil {
if err := medium.store.Set(newGroup, newKey, val); err != nil {
return err
}
return m.store.Delete(oldGroup, oldKey)
return medium.store.Delete(oldGroup, oldKey)
}
// Example: entries, _ := medium.List("app")
func (m *Medium) List(entryPath string) ([]fs.DirEntry, error) {
func (medium *Medium) List(entryPath string) ([]fs.DirEntry, error) {
group, key := splitGroupKeyPath(entryPath)
if group == "" {
rows, err := m.store.database.Query("SELECT DISTINCT grp FROM kv ORDER BY grp")
rows, err := medium.store.database.Query("SELECT DISTINCT grp FROM kv ORDER BY grp")
if err != nil {
return nil, core.E("store.List", "query groups", err)
}
@ -173,7 +173,7 @@ func (m *Medium) List(entryPath string) ([]fs.DirEntry, error) {
return nil, nil // leaf node, nothing beneath
}
all, err := m.store.GetAll(group)
all, err := medium.store.GetAll(group)
if err != nil {
return nil, err
}
@ -185,13 +185,13 @@ func (m *Medium) List(entryPath string) ([]fs.DirEntry, error) {
}
// Example: info, _ := medium.Stat("app/theme")
func (m *Medium) Stat(entryPath string) (fs.FileInfo, error) {
func (medium *Medium) Stat(entryPath string) (fs.FileInfo, error) {
group, key := splitGroupKeyPath(entryPath)
if group == "" {
return nil, core.E("store.Stat", "path is required", fs.ErrInvalid)
}
if key == "" {
entryCount, err := m.store.Count(group)
entryCount, err := medium.store.Count(group)
if err != nil {
return nil, err
}
@ -200,77 +200,77 @@ func (m *Medium) Stat(entryPath string) (fs.FileInfo, error) {
}
return &keyValueFileInfo{name: group, isDir: true}, nil
}
val, err := m.store.Get(group, key)
value, err := medium.store.Get(group, key)
if err != nil {
return nil, err
}
return &keyValueFileInfo{name: key, size: int64(len(val))}, nil
return &keyValueFileInfo{name: key, size: int64(len(value))}, nil
}
func (m *Medium) Open(entryPath string) (fs.File, error) {
func (medium *Medium) Open(entryPath string) (fs.File, error) {
group, key := splitGroupKeyPath(entryPath)
if key == "" {
return nil, core.E("store.Open", "path must include group/key", fs.ErrInvalid)
}
val, err := m.store.Get(group, key)
value, err := medium.store.Get(group, key)
if err != nil {
return nil, err
}
return &keyValueFile{name: key, content: []byte(val)}, nil
return &keyValueFile{name: key, content: []byte(value)}, nil
}
func (m *Medium) Create(entryPath string) (goio.WriteCloser, error) {
func (medium *Medium) Create(entryPath string) (goio.WriteCloser, error) {
group, key := splitGroupKeyPath(entryPath)
if key == "" {
return nil, core.E("store.Create", "path must include group/key", fs.ErrInvalid)
}
return &keyValueWriteCloser{store: m.store, group: group, key: key}, nil
return &keyValueWriteCloser{store: medium.store, group: group, key: key}, nil
}
func (m *Medium) Append(entryPath string) (goio.WriteCloser, error) {
func (medium *Medium) Append(entryPath string) (goio.WriteCloser, error) {
group, key := splitGroupKeyPath(entryPath)
if key == "" {
return nil, core.E("store.Append", "path must include group/key", fs.ErrInvalid)
}
existing, _ := m.store.Get(group, key)
return &keyValueWriteCloser{store: m.store, group: group, key: key, data: []byte(existing)}, nil
existingValue, _ := medium.store.Get(group, key)
return &keyValueWriteCloser{store: medium.store, group: group, key: key, data: []byte(existingValue)}, nil
}
func (m *Medium) ReadStream(entryPath string) (goio.ReadCloser, error) {
func (medium *Medium) ReadStream(entryPath string) (goio.ReadCloser, error) {
group, key := splitGroupKeyPath(entryPath)
if key == "" {
return nil, core.E("store.ReadStream", "path must include group/key", fs.ErrInvalid)
}
val, err := m.store.Get(group, key)
val, err := medium.store.Get(group, key)
if err != nil {
return nil, err
}
return goio.NopCloser(core.NewReader(val)), nil
}
func (m *Medium) WriteStream(entryPath string) (goio.WriteCloser, error) {
return m.Create(entryPath)
func (medium *Medium) WriteStream(entryPath string) (goio.WriteCloser, error) {
return medium.Create(entryPath)
}
func (m *Medium) Exists(entryPath string) bool {
func (medium *Medium) Exists(entryPath string) bool {
group, key := splitGroupKeyPath(entryPath)
if group == "" {
return false
}
if key == "" {
entryCount, err := m.store.Count(group)
entryCount, err := medium.store.Count(group)
return err == nil && entryCount > 0
}
_, err := m.store.Get(group, key)
_, err := medium.store.Get(group, key)
return err == nil
}
func (m *Medium) IsDir(entryPath string) bool {
func (medium *Medium) IsDir(entryPath string) bool {
group, key := splitGroupKeyPath(entryPath)
if key != "" || group == "" {
return false
}
entryCount, err := m.store.Count(group)
entryCount, err := medium.store.Count(group)
return err == nil && entryCount > 0
}
@ -282,22 +282,22 @@ type keyValueFileInfo struct {
isDir bool
}
func (fi *keyValueFileInfo) Name() string { return fi.name }
func (fileInfo *keyValueFileInfo) Name() string { return fileInfo.name }
func (fi *keyValueFileInfo) Size() int64 { return fi.size }
func (fileInfo *keyValueFileInfo) Size() int64 { return fileInfo.size }
func (fi *keyValueFileInfo) Mode() fs.FileMode {
if fi.isDir {
func (fileInfo *keyValueFileInfo) Mode() fs.FileMode {
if fileInfo.isDir {
return fs.ModeDir | 0755
}
return 0644
}
func (fi *keyValueFileInfo) ModTime() time.Time { return time.Time{} }
func (fileInfo *keyValueFileInfo) ModTime() time.Time { return time.Time{} }
func (fi *keyValueFileInfo) IsDir() bool { return fi.isDir }
func (fileInfo *keyValueFileInfo) IsDir() bool { return fileInfo.isDir }
func (fi *keyValueFileInfo) Sys() any { return nil }
func (fileInfo *keyValueFileInfo) Sys() any { return nil }
type keyValueDirEntry struct {
name string
@ -305,19 +305,19 @@ type keyValueDirEntry struct {
size int64
}
func (de *keyValueDirEntry) Name() string { return de.name }
func (entry *keyValueDirEntry) Name() string { return entry.name }
func (de *keyValueDirEntry) IsDir() bool { return de.isDir }
func (entry *keyValueDirEntry) IsDir() bool { return entry.isDir }
func (de *keyValueDirEntry) Type() fs.FileMode {
if de.isDir {
func (entry *keyValueDirEntry) Type() fs.FileMode {
if entry.isDir {
return fs.ModeDir
}
return 0
}
func (de *keyValueDirEntry) Info() (fs.FileInfo, error) {
return &keyValueFileInfo{name: de.name, size: de.size, isDir: de.isDir}, nil
func (entry *keyValueDirEntry) Info() (fs.FileInfo, error) {
return &keyValueFileInfo{name: entry.name, size: entry.size, isDir: entry.isDir}, nil
}
type keyValueFile struct {
@ -326,20 +326,20 @@ type keyValueFile struct {
offset int64
}
func (f *keyValueFile) Stat() (fs.FileInfo, error) {
return &keyValueFileInfo{name: f.name, size: int64(len(f.content))}, nil
func (file *keyValueFile) Stat() (fs.FileInfo, error) {
return &keyValueFileInfo{name: file.name, size: int64(len(file.content))}, nil
}
func (f *keyValueFile) Read(b []byte) (int, error) {
if f.offset >= int64(len(f.content)) {
func (file *keyValueFile) Read(buffer []byte) (int, error) {
if file.offset >= int64(len(file.content)) {
return 0, goio.EOF
}
n := copy(b, f.content[f.offset:])
f.offset += int64(n)
return n, nil
readCount := copy(buffer, file.content[file.offset:])
file.offset += int64(readCount)
return readCount, nil
}
func (f *keyValueFile) Close() error { return nil }
func (file *keyValueFile) Close() error { return nil }
type keyValueWriteCloser struct {
store *Store
@ -348,11 +348,11 @@ type keyValueWriteCloser struct {
data []byte
}
func (w *keyValueWriteCloser) Write(p []byte) (int, error) {
w.data = append(w.data, p...)
return len(p), nil
func (writer *keyValueWriteCloser) Write(data []byte) (int, error) {
writer.data = append(writer.data, data...)
return len(data), nil
}
func (w *keyValueWriteCloser) Close() error {
return w.store.Set(w.group, w.key, string(w.data))
func (writer *keyValueWriteCloser) Close() error {
return writer.store.Set(writer.group, writer.key, string(writer.data))
}

View file

@ -52,14 +52,14 @@ func New(options Options) (*Store, error) {
}
// Example: _ = keyValueStore.Close()
func (s *Store) Close() error {
return s.database.Close()
func (store *Store) Close() error {
return store.database.Close()
}
// Example: theme, _ := keyValueStore.Get("app", "theme")
func (s *Store) Get(group, key string) (string, error) {
func (store *Store) Get(group, key string) (string, error) {
var value string
err := s.database.QueryRow("SELECT value FROM kv WHERE grp = ? AND key = ?", group, key).Scan(&value)
err := store.database.QueryRow("SELECT value FROM kv WHERE grp = ? AND key = ?", group, key).Scan(&value)
if err == sql.ErrNoRows {
return "", core.E("store.Get", core.Concat("not found: ", group, "/", key), NotFoundError)
}
@ -70,8 +70,8 @@ func (s *Store) Get(group, key string) (string, error) {
}
// Example: _ = keyValueStore.Set("app", "theme", "midnight")
func (s *Store) Set(group, key, value string) error {
_, err := s.database.Exec(
func (store *Store) Set(group, key, value string) error {
_, err := store.database.Exec(
`INSERT INTO kv (grp, key, value) VALUES (?, ?, ?)
ON CONFLICT(grp, key) DO UPDATE SET value = excluded.value`,
group, key, value,
@ -83,8 +83,8 @@ func (s *Store) Set(group, key, value string) error {
}
// Example: _ = keyValueStore.Delete("app", "theme")
func (s *Store) Delete(group, key string) error {
_, err := s.database.Exec("DELETE FROM kv WHERE grp = ? AND key = ?", group, key)
func (store *Store) Delete(group, key string) error {
_, err := store.database.Exec("DELETE FROM kv WHERE grp = ? AND key = ?", group, key)
if err != nil {
return core.E("store.Delete", "exec", err)
}
@ -92,9 +92,9 @@ func (s *Store) Delete(group, key string) error {
}
// Example: count, _ := keyValueStore.Count("app")
func (s *Store) Count(group string) (int, error) {
func (store *Store) Count(group string) (int, error) {
var count int
err := s.database.QueryRow("SELECT COUNT(*) FROM kv WHERE grp = ?", group).Scan(&count)
err := store.database.QueryRow("SELECT COUNT(*) FROM kv WHERE grp = ?", group).Scan(&count)
if err != nil {
return 0, core.E("store.Count", "query", err)
}
@ -102,8 +102,8 @@ func (s *Store) Count(group string) (int, error) {
}
// Example: _ = keyValueStore.DeleteGroup("app")
func (s *Store) DeleteGroup(group string) error {
_, err := s.database.Exec("DELETE FROM kv WHERE grp = ?", group)
func (store *Store) DeleteGroup(group string) error {
_, err := store.database.Exec("DELETE FROM kv WHERE grp = ?", group)
if err != nil {
return core.E("store.DeleteGroup", "exec", err)
}
@ -111,8 +111,8 @@ func (s *Store) DeleteGroup(group string) error {
}
// Example: values, _ := keyValueStore.GetAll("app")
func (s *Store) GetAll(group string) (map[string]string, error) {
rows, err := s.database.Query("SELECT key, value FROM kv WHERE grp = ?", group)
func (store *Store) GetAll(group string) (map[string]string, error) {
rows, err := store.database.Query("SELECT key, value FROM kv WHERE grp = ?", group)
if err != nil {
return nil, core.E("store.GetAll", "query", err)
}
@ -135,8 +135,8 @@ func (s *Store) GetAll(group string) (map[string]string, error) {
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
// _ = keyValueStore.Set("user", "name", "alice")
// out, _ := keyValueStore.Render("hello {{ .name }}", "user")
func (s *Store) Render(templateText, group string) (string, error) {
rows, err := s.database.Query("SELECT key, value FROM kv WHERE grp = ?", group)
func (store *Store) Render(templateText, group string) (string, error) {
rows, err := store.database.Query("SELECT key, value FROM kv WHERE grp = ?", group)
if err != nil {
return "", core.E("store.Render", "query", err)
}

View file

@ -57,55 +57,55 @@ func New(options Options) (*Service, error) {
return nil, core.E("workspace.New", "core is required", fs.ErrInvalid)
}
s := &Service{
service := &Service{
core: options.Core,
rootPath: rootPath,
medium: io.Local,
}
if options.Crypt != nil {
s.crypt = options.Crypt
service.crypt = options.Crypt
}
if err := s.medium.EnsureDir(rootPath); err != nil {
if err := service.medium.EnsureDir(rootPath); err != nil {
return nil, core.E("workspace.New", "failed to ensure root directory", err)
}
return s, nil
return service, nil
}
// Example: workspaceID, _ := service.CreateWorkspace("alice", "pass123")
func (s *Service) CreateWorkspace(identifier, password string) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
func (service *Service) CreateWorkspace(identifier, password string) (string, error) {
service.mu.Lock()
defer service.mu.Unlock()
if s.crypt == nil {
if service.crypt == nil {
return "", core.E("workspace.CreateWorkspace", "crypt service not available", nil)
}
hash := sha256.Sum256([]byte(identifier))
workspaceID := hex.EncodeToString(hash[:])
workspaceDirectory, err := s.resolveWorkspaceDirectory("workspace.CreateWorkspace", workspaceID)
workspaceDirectory, err := service.resolveWorkspaceDirectory("workspace.CreateWorkspace", workspaceID)
if err != nil {
return "", err
}
if s.medium.Exists(workspaceDirectory) {
if service.medium.Exists(workspaceDirectory) {
return "", core.E("workspace.CreateWorkspace", "workspace already exists", nil)
}
for _, d := range []string{"config", "log", "data", "files", "keys"} {
if err := s.medium.EnsureDir(core.Path(workspaceDirectory, d)); err != nil {
if err := service.medium.EnsureDir(core.Path(workspaceDirectory, d)); err != nil {
return "", core.E("workspace.CreateWorkspace", core.Concat("failed to create directory: ", d), err)
}
}
privKey, err := s.crypt.CreateKeyPair(identifier, password)
privKey, err := service.crypt.CreateKeyPair(identifier, password)
if err != nil {
return "", core.E("workspace.CreateWorkspace", "failed to generate keys", err)
}
if err := s.medium.WriteMode(core.Path(workspaceDirectory, "keys", "private.key"), privKey, 0600); err != nil {
if err := service.medium.WriteMode(core.Path(workspaceDirectory, "keys", "private.key"), privKey, 0600); err != nil {
return "", core.E("workspace.CreateWorkspace", "failed to save private key", err)
}
@ -113,29 +113,29 @@ func (s *Service) CreateWorkspace(identifier, password string) (string, error) {
}
// Example: _ = service.SwitchWorkspace(workspaceID)
func (s *Service) SwitchWorkspace(workspaceID string) error {
s.mu.Lock()
defer s.mu.Unlock()
func (service *Service) SwitchWorkspace(workspaceID string) error {
service.mu.Lock()
defer service.mu.Unlock()
workspaceDirectory, err := s.resolveWorkspaceDirectory("workspace.SwitchWorkspace", workspaceID)
workspaceDirectory, err := service.resolveWorkspaceDirectory("workspace.SwitchWorkspace", workspaceID)
if err != nil {
return err
}
if !s.medium.IsDir(workspaceDirectory) {
if !service.medium.IsDir(workspaceDirectory) {
return core.E("workspace.SwitchWorkspace", core.Concat("workspace not found: ", workspaceID), nil)
}
s.activeWorkspaceID = core.PathBase(workspaceDirectory)
service.activeWorkspaceID = core.PathBase(workspaceDirectory)
return nil
}
// resolveActiveWorkspaceFilePath resolves a file path inside the active workspace files root.
// It rejects empty names and traversal outside the workspace root.
func (s *Service) resolveActiveWorkspaceFilePath(operation, workspaceFilePath string) (string, error) {
if s.activeWorkspaceID == "" {
func (service *Service) resolveActiveWorkspaceFilePath(operation, workspaceFilePath string) (string, error) {
if service.activeWorkspaceID == "" {
return "", core.E(operation, "no active workspace", nil)
}
filesRoot := core.Path(s.rootPath, s.activeWorkspaceID, "files")
filesRoot := core.Path(service.rootPath, service.activeWorkspaceID, "files")
filePath, err := joinPathWithinRoot(filesRoot, workspaceFilePath)
if err != nil {
return "", core.E(operation, "file path escapes workspace files", fs.ErrPermission)
@ -147,27 +147,27 @@ func (s *Service) resolveActiveWorkspaceFilePath(operation, workspaceFilePath st
}
// Example: content, _ := service.WorkspaceFileGet("notes/todo.txt")
func (s *Service) WorkspaceFileGet(workspaceFilePath string) (string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
func (service *Service) WorkspaceFileGet(workspaceFilePath string) (string, error) {
service.mu.RLock()
defer service.mu.RUnlock()
filePath, err := s.resolveActiveWorkspaceFilePath("workspace.WorkspaceFileGet", workspaceFilePath)
filePath, err := service.resolveActiveWorkspaceFilePath("workspace.WorkspaceFileGet", workspaceFilePath)
if err != nil {
return "", err
}
return s.medium.Read(filePath)
return service.medium.Read(filePath)
}
// Example: _ = service.WorkspaceFileSet("notes/todo.txt", "ship it")
func (s *Service) WorkspaceFileSet(workspaceFilePath, content string) error {
s.mu.Lock()
defer s.mu.Unlock()
func (service *Service) WorkspaceFileSet(workspaceFilePath, content string) error {
service.mu.Lock()
defer service.mu.Unlock()
filePath, err := s.resolveActiveWorkspaceFilePath("workspace.WorkspaceFileSet", workspaceFilePath)
filePath, err := service.resolveActiveWorkspaceFilePath("workspace.WorkspaceFileSet", workspaceFilePath)
if err != nil {
return err
}
return s.medium.Write(filePath, content)
return service.medium.Write(filePath, content)
}
// service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: myCryptProvider})
@ -185,7 +185,7 @@ func (s *Service) WorkspaceFileSet(workspaceFilePath, content string) error {
//
// _ = createResult.OK
// _ = switchResult.OK
func (s *Service) HandleIPCEvents(_ *core.Core, message core.Message) core.Result {
func (service *Service) HandleIPCEvents(_ *core.Core, message core.Message) core.Result {
switch payload := message.(type) {
case map[string]any:
action, _ := payload["action"].(string)
@ -193,14 +193,14 @@ func (s *Service) HandleIPCEvents(_ *core.Core, message core.Message) core.Resul
case "workspace.create":
identifier, _ := payload["identifier"].(string)
password, _ := payload["password"].(string)
workspaceID, err := s.CreateWorkspace(identifier, password)
workspaceID, err := service.CreateWorkspace(identifier, password)
if err != nil {
return core.Result{}.New(err)
}
return core.Result{Value: workspaceID, OK: true}
case "workspace.switch":
workspaceID, _ := payload["workspaceID"].(string)
if err := s.SwitchWorkspace(workspaceID); err != nil {
if err := service.SwitchWorkspace(workspaceID); err != nil {
return core.Result{}.New(err)
}
return core.Result{OK: true}
@ -228,15 +228,15 @@ func joinPathWithinRoot(root string, parts ...string) (string, error) {
return "", fs.ErrPermission
}
func (s *Service) resolveWorkspaceDirectory(operation, workspaceID string) (string, error) {
func (service *Service) resolveWorkspaceDirectory(operation, workspaceID string) (string, error) {
if workspaceID == "" {
return "", core.E(operation, "workspace id is required", fs.ErrInvalid)
}
workspaceDirectory, err := joinPathWithinRoot(s.rootPath, workspaceID)
workspaceDirectory, err := joinPathWithinRoot(service.rootPath, workspaceID)
if err != nil {
return "", core.E(operation, "workspace path escapes root", err)
}
if core.PathDir(workspaceDirectory) != s.rootPath {
if core.PathDir(workspaceDirectory) != service.rootPath {
return "", core.E(operation, core.Concat("invalid workspace id: ", workspaceID), fs.ErrPermission)
}
return workspaceDirectory, nil