diff --git a/path.go b/path.go new file mode 100644 index 0000000..0e62fb7 --- /dev/null +++ b/path.go @@ -0,0 +1,187 @@ +package tomo + +import "io" +import "os" +import "io/fs" +import "strings" +import "path/filepath" + +// FS is Tomo's implementation of fs.FS. It provides access to a specific part +// of the filesystem. +type FS struct { + path string +} + +// FileWriter is a writable version of fs.File. +type FileWriter interface { + interface { fs.File; io.Writer } +} + +// ApplicationUserDataFS returns an FS that an application can use to store user +// data files. +func ApplicationUserDataFS (app ApplicationDescription) (*FS, error) { + return appFs(userDataDir, app) +} + +// ApplicationUserConfigFS returns an FS that an application can use to store +// user configuration files. +func ApplicationUserConfigFS (app ApplicationDescription) (*FS, error) { + configDir, err := os.UserConfigDir() + if err != nil { return nil, err } + return appFs(configDir, app) +} + +// ApplicationUserCacheFS returns an FS that an application can use to store +// user cache files. +func ApplicationUserCacheFS (app ApplicationDescription) (*FS, error) { + cacheDir, err := os.UserCacheDir() + if err != nil { return nil, err } + return appFs(cacheDir, app) +} + +func pathErr (op, path string, err error) error { + return &fs.PathError { + Op: op, + Path: path, + Err: err, + } +} + +func appFs (root string, app ApplicationDescription) (*FS, error) { + // remove slashes + appname := app.String() + appname = strings.ReplaceAll(appname, "/", "-") + appname = strings.ReplaceAll(appname, "\\", "-") + + path := filepath.Join(root, appname) + + // ensure the directory actually exists + err := os.MkdirAll(path, 755) + if err != nil { return nil, err } + + return &FS { path: path }, nil +} + +func (this FS) subPath (name string) (string, error) { + if !fs.ValidPath(name) { return "", fs.ErrInvalid } + if strings.Contains(name, "/") { return "", fs.ErrInvalid } + return filepath.Join(this.path, name), nil +} + +// Open opens the named file. +func (this FS) Open (name string) (fs.File, error) { + path, err := this.subPath(name) + if err != nil { + return nil, pathErr("open", name, err) + } + + return os.Open(path) +} + +// Create creates or truncates the named file. +func (this FS) Create (name string) (FileWriter, error) { + path, err := this.subPath(name) + if err != nil { + return nil, pathErr("create", name, err) + } + + return os.Create(path) +} + +// OpenFile is the generalized open call; most users will use Open or Create +// instead. +func (this FS) OpenFile ( + name string, + flag int, + perm os.FileMode, +) ( + FileWriter, + error, +) { + path, err := this.subPath(name) + if err != nil { + return nil, pathErr("open", name, err) + } + + return os.OpenFile(path, flag, perm) +} + +// ReadDir reads the named directory and returns a list of directory entries +// sorted by filename. +func (this FS) ReadDir (name string) ([]fs.DirEntry, error) { + path, err := this.subPath(name) + if err != nil { + return nil, pathErr("readdir", name, err) + } + + return os.ReadDir(path) +} + +// ReadFile reads the named file and returns its contents. +// A successful call returns a nil error, not io.EOF. +// (Because ReadFile reads the whole file, the expected EOF +// from the final Read is not treated as an error to be reported.) +// +// The caller is permitted to modify the returned byte slice. +func (this FS) ReadFile (name string) ([]byte, error) { + path, err := this.subPath(name) + if err != nil { + return nil, pathErr("readfile", name, err) + } + + return os.ReadFile(path) +} + +// WriteFile writes data to the named file, creating it if necessary. +func (this FS) WriteFile (name string, data []byte, perm os.FileMode) error { + path, err := this.subPath(name) + if err != nil { + return pathErr("writefile", name, err) + } + + return os.WriteFile(path, data, perm) +} + +// Stat returns a FileInfo describing the file. +func (this FS) Stat (name string) (fs.FileInfo, error) { + path, err := this.subPath(name) + if err != nil { + return nil, pathErr("stat", name, err) + } + + return os.Stat(path) +} + +// Remove removes the named file or (empty) directory. +func (this FS) Remove (name string) error { + path, err := this.subPath(name) + if err != nil { + return pathErr("remove", name, err) + } + + return os.Remove(path) +} + +// RemoveAll removes name and any children it contains. +func (this FS) RemoveAll (name string) error { + path, err := this.subPath(name) + if err != nil { + return pathErr("removeall", name, err) + } + + return os.RemoveAll(path) +} + +// Rename renames (moves) oldname to newname. +func (this FS) Rename (oldname, newname string) error { + oldpath, err := this.subPath(oldname) + if err != nil { + return pathErr("rename", oldname, err) + } + newpath, err := this.subPath(newname) + if err != nil { + return pathErr("rename", newname, err) + } + + return os.Rename(oldpath, newpath) +} diff --git a/unix.go b/unix.go index 363bb6d..01da896 100644 --- a/unix.go +++ b/unix.go @@ -6,6 +6,8 @@ import "os" import "strings" import "path/filepath" +var userDataDir string + func init () { pathVariable := os.Getenv("TOMO_PLUGIN_PATH") pluginPaths = strings.Split(pathVariable, ":") @@ -19,4 +21,6 @@ func init () { pluginPaths, filepath.Join(homeDir, ".local/lib/tomo/plugins")) } + + userDataDir = filepath.Join(homeDir, ".local/share") }