diff --git a/io.go b/io.go index 85ddd91..0f69889 100644 --- a/io.go +++ b/io.go @@ -2,8 +2,10 @@ package io import ( "bytes" + "cmp" goio "io" "io/fs" + "slices" "time" core "dappco.re/go/core" @@ -176,13 +178,13 @@ func IsFile(medium Medium, path string) bool { return medium.IsFile(path) } -// Example: _ = io.Copy(source, "input.txt", destination, "backup/input.txt") -func Copy(source Medium, sourcePath string, destination Medium, destinationPath string) error { - content, err := source.Read(sourcePath) +// Example: _ = io.Copy(sourceMedium, "input.txt", destinationMedium, "backup/input.txt") +func Copy(sourceMedium Medium, sourcePath string, destinationMedium Medium, destinationPath string) error { + content, err := sourceMedium.Read(sourcePath) if err != nil { return core.E("io.Copy", core.Concat("read failed: ", sourcePath), err) } - if err := destination.Write(destinationPath, content); err != nil { + if err := destinationMedium.Write(destinationPath, content); err != nil { return core.E("io.Copy", core.Concat("write failed: ", destinationPath), err) } return nil @@ -191,10 +193,10 @@ func Copy(source Medium, sourcePath string, destination Medium, destinationPath // Example: medium := io.NewMemoryMedium() // Example: _ = medium.Write("config/app.yaml", "port: 8080") type MemoryMedium struct { - files map[string]string - modes map[string]fs.FileMode - dirs map[string]bool - modTimes map[string]time.Time + fileContents map[string]string + fileModes map[string]fs.FileMode + directories map[string]bool + modificationTimes map[string]time.Time } var _ Medium = (*MemoryMedium)(nil) @@ -203,15 +205,15 @@ var _ Medium = (*MemoryMedium)(nil) // Example: _ = medium.Write("config/app.yaml", "port: 8080") func NewMemoryMedium() *MemoryMedium { return &MemoryMedium{ - files: make(map[string]string), - modes: make(map[string]fs.FileMode), - dirs: make(map[string]bool), - modTimes: make(map[string]time.Time), + fileContents: make(map[string]string), + fileModes: make(map[string]fs.FileMode), + directories: make(map[string]bool), + modificationTimes: make(map[string]time.Time), } } func (medium *MemoryMedium) Read(path string) (string, error) { - content, ok := medium.files[path] + content, ok := medium.fileContents[path] if !ok { return "", core.E("io.MemoryMedium.Read", core.Concat("file not found: ", path), fs.ErrNotExist) } @@ -223,45 +225,45 @@ func (medium *MemoryMedium) Write(path, content string) error { } func (medium *MemoryMedium) WriteMode(path, content string, mode fs.FileMode) error { - medium.files[path] = content - medium.modes[path] = mode - medium.modTimes[path] = time.Now() + medium.fileContents[path] = content + medium.fileModes[path] = mode + medium.modificationTimes[path] = time.Now() return nil } func (medium *MemoryMedium) EnsureDir(path string) error { - medium.dirs[path] = true + medium.directories[path] = true return nil } func (medium *MemoryMedium) IsFile(path string) bool { - _, ok := medium.files[path] + _, ok := medium.fileContents[path] return ok } func (medium *MemoryMedium) Delete(path string) error { - if _, ok := medium.files[path]; ok { - delete(medium.files, path) - delete(medium.modes, path) - delete(medium.modTimes, path) + if _, ok := medium.fileContents[path]; ok { + delete(medium.fileContents, path) + delete(medium.fileModes, path) + delete(medium.modificationTimes, path) return nil } - if _, ok := medium.dirs[path]; ok { + if _, ok := medium.directories[path]; ok { prefix := path if !core.HasSuffix(prefix, "/") { prefix += "/" } - for filePath := range medium.files { + for filePath := range medium.fileContents { if core.HasPrefix(filePath, prefix) { return core.E("io.MemoryMedium.Delete", core.Concat("directory not empty: ", path), fs.ErrExist) } } - for directoryPath := range medium.dirs { + for directoryPath := range medium.directories { if directoryPath != path && core.HasPrefix(directoryPath, prefix) { return core.E("io.MemoryMedium.Delete", core.Concat("directory not empty: ", path), fs.ErrExist) } } - delete(medium.dirs, path) + delete(medium.directories, path) return nil } return core.E("io.MemoryMedium.Delete", core.Concat("path not found: ", path), fs.ErrNotExist) @@ -269,31 +271,31 @@ func (medium *MemoryMedium) Delete(path string) error { func (medium *MemoryMedium) DeleteAll(path string) error { found := false - if _, ok := medium.files[path]; ok { - delete(medium.files, path) - delete(medium.modes, path) - delete(medium.modTimes, path) + if _, ok := medium.fileContents[path]; ok { + delete(medium.fileContents, path) + delete(medium.fileModes, path) + delete(medium.modificationTimes, path) found = true } - if _, ok := medium.dirs[path]; ok { - delete(medium.dirs, path) + if _, ok := medium.directories[path]; ok { + delete(medium.directories, path) found = true } prefix := path if !core.HasSuffix(prefix, "/") { prefix += "/" } - for filePath := range medium.files { + for filePath := range medium.fileContents { if core.HasPrefix(filePath, prefix) { - delete(medium.files, filePath) - delete(medium.modes, filePath) - delete(medium.modTimes, filePath) + delete(medium.fileContents, filePath) + delete(medium.fileModes, filePath) + delete(medium.modificationTimes, filePath) found = true } } - for directoryPath := range medium.dirs { + for directoryPath := range medium.directories { if core.HasPrefix(directoryPath, prefix) { - delete(medium.dirs, directoryPath) + delete(medium.directories, directoryPath) found = true } } @@ -305,22 +307,22 @@ func (medium *MemoryMedium) DeleteAll(path string) error { } func (medium *MemoryMedium) Rename(oldPath, newPath string) error { - if content, ok := medium.files[oldPath]; ok { - medium.files[newPath] = content - delete(medium.files, oldPath) - if mode, ok := medium.modes[oldPath]; ok { - medium.modes[newPath] = mode - delete(medium.modes, oldPath) + if content, ok := medium.fileContents[oldPath]; ok { + medium.fileContents[newPath] = content + delete(medium.fileContents, oldPath) + if mode, ok := medium.fileModes[oldPath]; ok { + medium.fileModes[newPath] = mode + delete(medium.fileModes, oldPath) } - if modTime, ok := medium.modTimes[oldPath]; ok { - medium.modTimes[newPath] = modTime - delete(medium.modTimes, oldPath) + if modTime, ok := medium.modificationTimes[oldPath]; ok { + medium.modificationTimes[newPath] = modTime + delete(medium.modificationTimes, oldPath) } return nil } - if _, ok := medium.dirs[oldPath]; ok { - medium.dirs[newPath] = true - delete(medium.dirs, oldPath) + if _, ok := medium.directories[oldPath]; ok { + medium.directories[newPath] = true + delete(medium.directories, oldPath) oldPrefix := oldPath if !core.HasSuffix(oldPrefix, "/") { @@ -332,31 +334,31 @@ func (medium *MemoryMedium) Rename(oldPath, newPath string) error { } filesToMove := make(map[string]string) - for filePath := range medium.files { + for filePath := range medium.fileContents { if core.HasPrefix(filePath, oldPrefix) { newFilePath := core.Concat(newPrefix, core.TrimPrefix(filePath, oldPrefix)) filesToMove[filePath] = newFilePath } } 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) + medium.fileContents[newFilePath] = medium.fileContents[oldFilePath] + delete(medium.fileContents, oldFilePath) + if modTime, ok := medium.modificationTimes[oldFilePath]; ok { + medium.modificationTimes[newFilePath] = modTime + delete(medium.modificationTimes, oldFilePath) } } dirsToMove := make(map[string]string) - for directoryPath := range medium.dirs { + for directoryPath := range medium.directories { if core.HasPrefix(directoryPath, oldPrefix) { newDirectoryPath := core.Concat(newPrefix, core.TrimPrefix(directoryPath, oldPrefix)) dirsToMove[directoryPath] = newDirectoryPath } } for oldDirectoryPath, newDirectoryPath := range dirsToMove { - medium.dirs[newDirectoryPath] = true - delete(medium.dirs, oldDirectoryPath) + medium.directories[newDirectoryPath] = true + delete(medium.directories, oldDirectoryPath) } return nil } @@ -364,7 +366,7 @@ func (medium *MemoryMedium) Rename(oldPath, newPath string) error { } func (medium *MemoryMedium) Open(path string) (fs.File, error) { - content, ok := medium.files[path] + content, ok := medium.fileContents[path] if !ok { return nil, core.E("io.MemoryMedium.Open", core.Concat("file not found: ", path), fs.ErrNotExist) } @@ -384,7 +386,7 @@ func (medium *MemoryMedium) Create(path string) (goio.WriteCloser, error) { } func (medium *MemoryMedium) Append(path string) (goio.WriteCloser, error) { - content := medium.files[path] + content := medium.fileContents[path] return &MemoryWriteCloser{ medium: medium, path: path, @@ -442,34 +444,34 @@ func (writeCloser *MemoryWriteCloser) Write(data []byte) (int, error) { } func (writeCloser *MemoryWriteCloser) Close() error { - writeCloser.medium.files[writeCloser.path] = string(writeCloser.data) - writeCloser.medium.modes[writeCloser.path] = writeCloser.mode - writeCloser.medium.modTimes[writeCloser.path] = time.Now() + writeCloser.medium.fileContents[writeCloser.path] = string(writeCloser.data) + writeCloser.medium.fileModes[writeCloser.path] = writeCloser.mode + writeCloser.medium.modificationTimes[writeCloser.path] = time.Now() return nil } func (medium *MemoryMedium) fileMode(path string) fs.FileMode { - if mode, ok := medium.modes[path]; ok { + if mode, ok := medium.fileModes[path]; ok { return mode } return 0644 } func (medium *MemoryMedium) List(path string) ([]fs.DirEntry, error) { - if _, ok := medium.dirs[path]; !ok { + if _, ok := medium.directories[path]; !ok { hasChildren := false prefix := path if path != "" && !core.HasSuffix(prefix, "/") { prefix += "/" } - for filePath := range medium.files { + for filePath := range medium.fileContents { if core.HasPrefix(filePath, prefix) { hasChildren = true break } } if !hasChildren { - for directoryPath := range medium.dirs { + for directoryPath := range medium.directories { if core.HasPrefix(directoryPath, prefix) { hasChildren = true break @@ -489,7 +491,7 @@ func (medium *MemoryMedium) List(path string) ([]fs.DirEntry, error) { seen := make(map[string]bool) var entries []fs.DirEntry - for filePath, content := range medium.files { + for filePath, content := range medium.fileContents { if !core.HasPrefix(filePath, prefix) { continue } @@ -521,7 +523,7 @@ func (medium *MemoryMedium) List(path string) ([]fs.DirEntry, error) { } } - for directoryPath := range medium.dirs { + for directoryPath := range medium.directories { if !core.HasPrefix(directoryPath, prefix) { continue } @@ -543,34 +545,38 @@ func (medium *MemoryMedium) List(path string) ([]fs.DirEntry, error) { } } + slices.SortFunc(entries, func(a, b fs.DirEntry) int { + return cmp.Compare(a.Name(), b.Name()) + }) + return entries, nil } func (medium *MemoryMedium) Stat(path string) (fs.FileInfo, error) { - if content, ok := medium.files[path]; ok { - modTime, ok := medium.modTimes[path] + if content, ok := medium.fileContents[path]; ok { + modTime, ok := medium.modificationTimes[path] if !ok { modTime = time.Now() } return NewFileInfo(core.PathBase(path), int64(len(content)), medium.fileMode(path), modTime, false), nil } - if _, ok := medium.dirs[path]; ok { + if _, ok := medium.directories[path]; ok { return NewFileInfo(core.PathBase(path), 0, fs.ModeDir|0755, time.Time{}, true), nil } return nil, core.E("io.MemoryMedium.Stat", core.Concat("path not found: ", path), fs.ErrNotExist) } func (medium *MemoryMedium) Exists(path string) bool { - if _, ok := medium.files[path]; ok { + if _, ok := medium.fileContents[path]; ok { return true } - if _, ok := medium.dirs[path]; ok { + if _, ok := medium.directories[path]; ok { return true } return false } func (medium *MemoryMedium) IsDir(path string) bool { - _, ok := medium.dirs[path] + _, ok := medium.directories[path] return ok } diff --git a/medium_test.go b/medium_test.go index 3a5c164..26f07d3 100644 --- a/medium_test.go +++ b/medium_test.go @@ -13,10 +13,10 @@ import ( func TestMemoryMedium_NewMemoryMedium_Good(t *testing.T) { memoryMedium := NewMemoryMedium() assert.NotNil(t, memoryMedium) - assert.NotNil(t, memoryMedium.files) - assert.NotNil(t, memoryMedium.dirs) - assert.Empty(t, memoryMedium.files) - assert.Empty(t, memoryMedium.dirs) + assert.NotNil(t, memoryMedium.fileContents) + assert.NotNil(t, memoryMedium.directories) + assert.Empty(t, memoryMedium.fileContents) + assert.Empty(t, memoryMedium.directories) } func TestMemoryMedium_NewFileInfo_Good(t *testing.T) { @@ -46,7 +46,7 @@ func TestMemoryMedium_NewDirEntry_Good(t *testing.T) { func TestMemoryMedium_Read_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["test.txt"] = "hello world" + memoryMedium.fileContents["test.txt"] = "hello world" content, err := memoryMedium.Read("test.txt") assert.NoError(t, err) assert.Equal(t, "hello world", content) @@ -62,11 +62,11 @@ func TestMemoryMedium_Write_Good(t *testing.T) { memoryMedium := NewMemoryMedium() err := memoryMedium.Write("test.txt", "content") assert.NoError(t, err) - assert.Equal(t, "content", memoryMedium.files["test.txt"]) + assert.Equal(t, "content", memoryMedium.fileContents["test.txt"]) err = memoryMedium.Write("test.txt", "new content") assert.NoError(t, err) - assert.Equal(t, "new content", memoryMedium.files["test.txt"]) + assert.Equal(t, "new content", memoryMedium.fileContents["test.txt"]) } func TestMemoryMedium_WriteMode_Good(t *testing.T) { @@ -94,12 +94,12 @@ func TestMemoryMedium_EnsureDir_Good(t *testing.T) { memoryMedium := NewMemoryMedium() err := memoryMedium.EnsureDir("/path/to/dir") assert.NoError(t, err) - assert.True(t, memoryMedium.dirs["/path/to/dir"]) + assert.True(t, memoryMedium.directories["/path/to/dir"]) } func TestMemoryMedium_IsFile_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["exists.txt"] = "content" + memoryMedium.fileContents["exists.txt"] = "content" assert.True(t, memoryMedium.IsFile("exists.txt")) assert.False(t, memoryMedium.IsFile("nonexistent.txt")) @@ -107,7 +107,7 @@ func TestMemoryMedium_IsFile_Good(t *testing.T) { func TestMemoryMedium_Delete_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["test.txt"] = "content" + memoryMedium.fileContents["test.txt"] = "content" err := memoryMedium.Delete("test.txt") assert.NoError(t, err) @@ -122,8 +122,8 @@ func TestMemoryMedium_Delete_NotFound_Bad(t *testing.T) { func TestMemoryMedium_Delete_DirNotEmpty_Bad(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.dirs["mydir"] = true - memoryMedium.files["mydir/file.txt"] = "content" + memoryMedium.directories["mydir"] = true + memoryMedium.fileContents["mydir/file.txt"] = "content" err := memoryMedium.Delete("mydir") assert.Error(t, err) @@ -131,50 +131,53 @@ func TestMemoryMedium_Delete_DirNotEmpty_Bad(t *testing.T) { func TestMemoryMedium_DeleteAll_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.dirs["mydir"] = true - memoryMedium.dirs["mydir/subdir"] = true - memoryMedium.files["mydir/file.txt"] = "content" - memoryMedium.files["mydir/subdir/nested.txt"] = "nested" + memoryMedium.directories["mydir"] = true + memoryMedium.directories["mydir/subdir"] = true + memoryMedium.fileContents["mydir/file.txt"] = "content" + memoryMedium.fileContents["mydir/subdir/nested.txt"] = "nested" err := memoryMedium.DeleteAll("mydir") assert.NoError(t, err) - assert.Empty(t, memoryMedium.dirs) - assert.Empty(t, memoryMedium.files) + assert.Empty(t, memoryMedium.directories) + assert.Empty(t, memoryMedium.fileContents) } func TestMemoryMedium_Rename_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["old.txt"] = "content" + memoryMedium.fileContents["old.txt"] = "content" err := memoryMedium.Rename("old.txt", "new.txt") assert.NoError(t, err) assert.False(t, memoryMedium.IsFile("old.txt")) assert.True(t, memoryMedium.IsFile("new.txt")) - assert.Equal(t, "content", memoryMedium.files["new.txt"]) + assert.Equal(t, "content", memoryMedium.fileContents["new.txt"]) } func TestMemoryMedium_Rename_Dir_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.dirs["olddir"] = true - memoryMedium.files["olddir/file.txt"] = "content" + memoryMedium.directories["olddir"] = true + memoryMedium.fileContents["olddir/file.txt"] = "content" err := memoryMedium.Rename("olddir", "newdir") assert.NoError(t, err) - assert.False(t, memoryMedium.dirs["olddir"]) - assert.True(t, memoryMedium.dirs["newdir"]) - assert.Equal(t, "content", memoryMedium.files["newdir/file.txt"]) + assert.False(t, memoryMedium.directories["olddir"]) + assert.True(t, memoryMedium.directories["newdir"]) + assert.Equal(t, "content", memoryMedium.fileContents["newdir/file.txt"]) } func TestMemoryMedium_List_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.dirs["mydir"] = true - memoryMedium.files["mydir/file1.txt"] = "content1" - memoryMedium.files["mydir/file2.txt"] = "content2" - memoryMedium.dirs["mydir/subdir"] = true + memoryMedium.directories["mydir"] = true + memoryMedium.fileContents["mydir/file1.txt"] = "content1" + memoryMedium.fileContents["mydir/file2.txt"] = "content2" + memoryMedium.directories["mydir/subdir"] = true entries, err := memoryMedium.List("mydir") assert.NoError(t, err) assert.Len(t, entries, 3) + assert.Equal(t, "file1.txt", entries[0].Name()) + assert.Equal(t, "file2.txt", entries[1].Name()) + assert.Equal(t, "subdir", entries[2].Name()) names := make(map[string]bool) for _, entry := range entries { @@ -187,7 +190,7 @@ func TestMemoryMedium_List_Good(t *testing.T) { func TestMemoryMedium_Stat_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["test.txt"] = "hello world" + memoryMedium.fileContents["test.txt"] = "hello world" info, err := memoryMedium.Stat("test.txt") assert.NoError(t, err) @@ -198,7 +201,7 @@ func TestMemoryMedium_Stat_Good(t *testing.T) { func TestMemoryMedium_Stat_Dir_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.dirs["mydir"] = true + memoryMedium.directories["mydir"] = true info, err := memoryMedium.Stat("mydir") assert.NoError(t, err) @@ -208,8 +211,8 @@ func TestMemoryMedium_Stat_Dir_Good(t *testing.T) { func TestMemoryMedium_Exists_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["file.txt"] = "content" - memoryMedium.dirs["mydir"] = true + memoryMedium.fileContents["file.txt"] = "content" + memoryMedium.directories["mydir"] = true assert.True(t, memoryMedium.Exists("file.txt")) assert.True(t, memoryMedium.Exists("mydir")) @@ -218,8 +221,8 @@ func TestMemoryMedium_Exists_Good(t *testing.T) { func TestMemoryMedium_IsDir_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["file.txt"] = "content" - memoryMedium.dirs["mydir"] = true + memoryMedium.fileContents["file.txt"] = "content" + memoryMedium.directories["mydir"] = true assert.False(t, memoryMedium.IsDir("file.txt")) assert.True(t, memoryMedium.IsDir("mydir")) @@ -286,7 +289,7 @@ func TestMemoryMedium_StreamAndFSHelpers_Good(t *testing.T) { require.NoError(t, err) require.NoError(t, writeStream.Close()) - assert.Equal(t, "stream output", memoryMedium.files["streamed.txt"]) + assert.Equal(t, "stream output", memoryMedium.fileContents["streamed.txt"]) statInfo, err := memoryMedium.Stat("streamed.txt") require.NoError(t, err) assert.Equal(t, fs.FileMode(0644), statInfo.Mode()) @@ -294,7 +297,7 @@ func TestMemoryMedium_StreamAndFSHelpers_Good(t *testing.T) { func TestIO_Read_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["test.txt"] = "hello" + memoryMedium.fileContents["test.txt"] = "hello" content, err := Read(memoryMedium, "test.txt") assert.NoError(t, err) assert.Equal(t, "hello", content) @@ -304,19 +307,19 @@ func TestIO_Write_Good(t *testing.T) { memoryMedium := NewMemoryMedium() err := Write(memoryMedium, "test.txt", "hello") assert.NoError(t, err) - assert.Equal(t, "hello", memoryMedium.files["test.txt"]) + assert.Equal(t, "hello", memoryMedium.fileContents["test.txt"]) } func TestIO_EnsureDir_Good(t *testing.T) { memoryMedium := NewMemoryMedium() err := EnsureDir(memoryMedium, "/my/dir") assert.NoError(t, err) - assert.True(t, memoryMedium.dirs["/my/dir"]) + assert.True(t, memoryMedium.directories["/my/dir"]) } func TestIO_IsFile_Good(t *testing.T) { memoryMedium := NewMemoryMedium() - memoryMedium.files["exists.txt"] = "content" + memoryMedium.fileContents["exists.txt"] = "content" assert.True(t, IsFile(memoryMedium, "exists.txt")) assert.False(t, IsFile(memoryMedium, "nonexistent.txt")) @@ -356,15 +359,15 @@ func TestIO_ReadWriteStream_Good(t *testing.T) { func TestIO_Copy_Good(t *testing.T) { source := NewMemoryMedium() dest := NewMemoryMedium() - source.files["test.txt"] = "hello" + source.fileContents["test.txt"] = "hello" err := Copy(source, "test.txt", dest, "test.txt") assert.NoError(t, err) - assert.Equal(t, "hello", dest.files["test.txt"]) + assert.Equal(t, "hello", dest.fileContents["test.txt"]) - source.files["original.txt"] = "content" + source.fileContents["original.txt"] = "content" err = Copy(source, "original.txt", dest, "copied.txt") assert.NoError(t, err) - assert.Equal(t, "content", dest.files["copied.txt"]) + assert.Equal(t, "content", dest.fileContents["copied.txt"]) } func TestIO_Copy_Bad(t *testing.T) {