// 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 `yaml:"path"` Owner string `yaml:"owner"` Group string `yaml:"group"` Mode string `yaml:"mode"` Atime time.Time `yaml:"atime",omitempty` Ctime time.Time `yaml:"ctime",omitempty` Mtime time.Time `yaml:"mtime",omitempty` Content string `yaml:"content",omitempty` FileType FileType `yaml:"filetype"` State string `yaml:"state"` } func NewFile() *File { return &File{loader: YamlLoadDecl, 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 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) 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 DirectoryFile: os.MkdirAll(f.Path, os.FileMode(mode)) 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) Read(ctx context.Context) ([]byte, error) { filePath, fileAbsErr := filepath.Abs(f.Path) if fileAbsErr != nil { panic(fileAbsErr) } f.Path = filePath info, e := os.Stat(f.Path) if e != nil { f.State = "absent" return nil, 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()) 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) 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") } }