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 { 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) }