// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "errors" "fmt" "gopkg.in/yaml.v3" "io" "net/url" "os" "os/user" "path/filepath" "strconv" "syscall" "time" ) type FileType string const ( RegularFile FileType = "regular" DirectoryFile FileType = "directory" BlockDeviceFile FileType = "block" CharacterDeviceFile FileType = "char" NamedPipeFile FileType = "pipe" SymbolicLinkFile FileType = "symlink" SocketFile FileType = "socket" ) var ErrInvalidResourceURI error = errors.New("Invalid resource URI") func init() { ResourceTypes.Register("file", func(u *url.URL) Resource { f := NewFile() f.Path = filepath.Join(u.Hostname(), u.Path) return f }) } // Manage the state of file system objects type File struct { loader YamlLoader Path string `json:"path" yaml:"path"` Owner string `json:"owner" yaml:"owner"` Group string `json:"group" yaml:"group"` Mode string `json:"mode" yaml:"mode"` Atime time.Time `json:"atime,omitempty" yaml:"atime,omitempty"` Ctime time.Time `json:"ctime,omitempty" yaml:"ctime,omitempty"` Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"` Content string `json:"content,omitempty" yaml:"content,omitempty"` Target string `json:"target,omitempty" yaml:"target,omitempty"` FileType FileType `json:"filetype" yaml:"filetype"` State string `json:"state" yaml:"state"` } func NewFile() *File { currentUser, _ := user.Current() group, _ := user.LookupGroupId(currentUser.Gid) return &File{loader: YamlLoadDecl, Owner: currentUser.Username, Group: group.Name, Mode: "0666", FileType: RegularFile} } func (f *File) URI() string { return fmt.Sprintf("file://%s", f.Path) } func (f *File) SetURI(uri string) error { resourceUri, e := url.Parse(uri) if e == nil { if resourceUri.Scheme == "file" { f.Path, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())) } else { e = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, uri) } } return e } func (f *File) Validate() error { return fmt.Errorf("failed") } func (f *File) Apply() error { switch f.State { case "absent": removeErr := os.Remove(f.Path) if removeErr != nil { return removeErr } case "present": { uid, uidErr := LookupUID(f.Owner) if uidErr != nil { return uidErr } gid, gidErr := LookupGID(f.Group) if gidErr != nil { return gidErr } mode, modeErr := strconv.ParseInt(f.Mode, 8, 64) if modeErr != nil { return modeErr } //e := os.Stat(f.path) //if os.IsNotExist(e) { switch f.FileType { case SymbolicLinkFile: linkErr := os.Symlink(f.Target, f.Path) if linkErr != nil { return linkErr } case DirectoryFile: if mkdirErr := os.MkdirAll(f.Path, os.FileMode(mode)); mkdirErr != nil { return mkdirErr } default: fallthrough case RegularFile: createdFile, e := os.Create(f.Path) if e != nil { return e } defer createdFile.Close() if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil { return chmodErr } _, writeErr := createdFile.Write([]byte(f.Content)) if writeErr != nil { return writeErr } if !f.Mtime.IsZero() && !f.Atime.IsZero() { if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil { return chtimesErr } } } if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil { return chownErr } } } return nil } func (f *File) LoadDecl(yamlFileResourceDeclaration string) error { return f.loader(yamlFileResourceDeclaration, f) } func (f *File) ResolveId(ctx context.Context) string { filePath, fileAbsErr := filepath.Abs(f.Path) if fileAbsErr != nil { panic(fileAbsErr) } f.Path = filePath return filePath } func (f *File) NormalizePath() error { filePath, fileAbsErr := filepath.Abs(f.Path) if fileAbsErr == nil { f.Path = filePath } return fileAbsErr } func (f *File) ReadStat() error { info, e := os.Lstat(f.Path) if e != nil { f.State = "absent" return e } f.Mtime = info.ModTime() if stat, ok := info.Sys().(*syscall.Stat_t); ok { f.Atime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) f.Ctime = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) userId := strconv.Itoa(int(stat.Uid)) groupId := strconv.Itoa(int(stat.Gid)) fileUser, userErr := user.LookupId(userId) if userErr != nil { //UnknownUserIdError //panic(userErr) f.Owner = userId } else { f.Owner = fileUser.Name } fileGroup, groupErr := user.LookupGroupId(groupId) if groupErr != nil { //panic(groupErr) f.Group = groupId } else { f.Group = fileGroup.Name } } f.Mode = fmt.Sprintf("%04o", info.Mode().Perm()) f.FileType.SetMode(info.Mode()) return nil } func (f *File) Read(ctx context.Context) ([]byte, error) { if normalizePathErr := f.NormalizePath(); normalizePathErr != nil { return nil, normalizePathErr } statErr := f.ReadStat() if statErr != nil { return nil, statErr } switch f.FileType { case RegularFile: file, fileErr := os.Open(f.Path) if fileErr != nil { panic(fileErr) } fileContent, ioErr := io.ReadAll(file) if ioErr != nil { panic(ioErr) } f.Content = string(fileContent) case SymbolicLinkFile: linkTarget, pathErr := os.Readlink(f.Path) if pathErr != nil { return nil, pathErr } f.Target = linkTarget } f.State = "present" return yaml.Marshal(f) } func (f *File) Type() string { return "file" } func (f *FileType) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } switch s { case string(RegularFile), string(DirectoryFile), string(BlockDeviceFile), string(CharacterDeviceFile), string(NamedPipeFile), string(SymbolicLinkFile), string(SocketFile): *f = FileType(s) return nil default: return errors.New("invalid FileType value") } } func (f *FileType) SetMode(mode os.FileMode) { switch true { case mode.IsRegular(): *f = RegularFile case mode.IsDir(): *f = DirectoryFile case mode&os.ModeSymlink != 0: *f = SymbolicLinkFile case mode&os.ModeNamedPipe != 0: *f = NamedPipeFile case mode&os.ModeSocket != 0: *f = SocketFile case mode&os.ModeCharDevice != 0: *f = CharacterDeviceFile case mode&os.ModeDevice != 0: *f = BlockDeviceFile } }