refactor(ax): clarify core storage names
This commit is contained in:
parent
a8eaaa1581
commit
bab889e9ac
13 changed files with 704 additions and 723 deletions
|
|
@ -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
2
doc.go
|
|
@ -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
284
io.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
103
local/client.go
103
local/client.go
|
|
@ -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) {
|
||||
|
|
|
|||
144
node/node.go
144
node/node.go
|
|
@ -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
162
s3/s3.go
|
|
@ -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),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
128
sqlite/sqlite.go
128
sqlite/sqlite.go
|
|
@ -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
|
||||
|
|
|
|||
158
store/medium.go
158
store/medium.go
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue