174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
package fs
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"codespace/internal/util"
|
|
)
|
|
|
|
// LocalFS implements FileSystem bound to a single workspace root directory.
|
|
type LocalFS struct {
|
|
root string
|
|
}
|
|
|
|
// NewLocal creates a LocalFS whose root is absolute and cleaned if possible.
|
|
func NewLocal(root string) *LocalFS {
|
|
abs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
abs = filepath.Clean(root)
|
|
}
|
|
return &LocalFS{root: abs}
|
|
}
|
|
|
|
// resolve validates path, cleans it, and returns the absolute path on disk
|
|
// plus the workspace-relative form. It rejects paths that escape the root.
|
|
func (l *LocalFS) resolve(path string) (abs string, rel string, err error) {
|
|
rel, err = util.CleanRelative(path)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
abs = filepath.Join(l.root, rel)
|
|
// Final containment check: ensure abs is within root.
|
|
if abs != l.root && !strings.HasPrefix(abs, l.root+string(filepath.Separator)) {
|
|
return "", "", util.New(util.CodeBadRequest, "path escapes workspace root")
|
|
}
|
|
return abs, rel, nil
|
|
}
|
|
|
|
// List returns sorted directory entries for path.
|
|
func (l *LocalFS) List(path string) ([]FileInfo, error) {
|
|
abs, rel, err := l.resolve(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries, err := os.ReadDir(abs)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, util.New(util.CodeNotFound, "path not found")
|
|
}
|
|
return nil, util.Wrap(util.CodeInternal, "failed to list directory", err)
|
|
}
|
|
result := make([]FileInfo, 0, len(entries))
|
|
for _, e := range entries {
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
return nil, util.Wrap(util.CodeInternal, "failed to get file info", err)
|
|
}
|
|
result = append(result, FileInfo{
|
|
Name: e.Name(),
|
|
Path: filepath.Join(rel, e.Name()),
|
|
IsDir: e.IsDir(),
|
|
Size: info.Size(),
|
|
ModTime: info.ModTime(),
|
|
})
|
|
}
|
|
sort.Slice(result, func(i, j int) bool {
|
|
return result[i].Name < result[j].Name
|
|
})
|
|
return result, nil
|
|
}
|
|
|
|
// Read returns the contents of path.
|
|
func (l *LocalFS) Read(path string) ([]byte, error) {
|
|
abs, _, err := l.resolve(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := os.ReadFile(abs)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, util.New(util.CodeNotFound, "file not found")
|
|
}
|
|
return nil, util.Wrap(util.CodeInternal, "failed to read file", err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// Write writes data to path, creating parent directories as needed.
|
|
func (l *LocalFS) Write(path string, data []byte) error {
|
|
abs, _, err := l.resolve(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parent := filepath.Dir(abs)
|
|
if err := os.MkdirAll(parent, 0o755); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to create parent directories", err)
|
|
}
|
|
if err := os.WriteFile(abs, data, 0o644); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to write file", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Mkdir creates a directory at path.
|
|
func (l *LocalFS) Mkdir(path string) error {
|
|
abs, _, err := l.resolve(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(abs, 0o755); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to create directory", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Remove removes path (file or directory recursively).
|
|
func (l *LocalFS) Remove(path string) error {
|
|
abs, rel, err := l.resolve(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rel == "." {
|
|
return util.New(util.CodeBadRequest, "cannot remove workspace root")
|
|
}
|
|
if err := os.RemoveAll(abs); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to remove path", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Rename moves oldPath to newPath, creating destination parent dirs.
|
|
func (l *LocalFS) Rename(oldPath, newPath string) error {
|
|
oldAbs, _, err := l.resolve(oldPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newAbs, _, err := l.resolve(newPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parent := filepath.Dir(newAbs)
|
|
if err := os.MkdirAll(parent, 0o755); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to create parent directories", err)
|
|
}
|
|
if err := os.Rename(oldAbs, newAbs); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to rename", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Stat returns metadata for path.
|
|
func (l *LocalFS) Stat(path string) (*FileInfo, error) {
|
|
abs, rel, err := l.resolve(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, err := os.Stat(abs)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, util.New(util.CodeNotFound, "path not found")
|
|
}
|
|
return nil, util.Wrap(util.CodeInternal, "failed to stat path", err)
|
|
}
|
|
return &FileInfo{
|
|
Name: filepath.Base(rel),
|
|
Path: rel,
|
|
IsDir: info.IsDir(),
|
|
Size: info.Size(),
|
|
ModTime: info.ModTime(),
|
|
}, nil
|
|
}
|