Matthew Rich
e695278d0c
All checks were successful
Declarative Tests / test (push) Successful in 48s
223 lines
4.7 KiB
Go
223 lines
4.7 KiB
Go
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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")
|
|
}
|
|
}
|