2024-03-20 19:23:31 +00:00
|
|
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
2024-03-22 17:39:06 +00:00
|
|
|
|
2024-03-20 16:15:27 +00:00
|
|
|
package resource
|
|
|
|
|
|
|
|
import (
|
2024-03-25 20:31:06 +00:00
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"io"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"os/user"
|
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
2024-03-20 16:15:27 +00:00
|
|
|
)
|
|
|
|
|
2024-03-20 19:23:31 +00:00
|
|
|
type FileType string
|
|
|
|
|
|
|
|
const (
|
2024-03-25 20:31:06 +00:00
|
|
|
RegularFile FileType = "regular"
|
|
|
|
DirectoryFile FileType = "directory"
|
|
|
|
BlockDeviceFile FileType = "block"
|
|
|
|
CharacterDeviceFile FileType = "char"
|
|
|
|
NamedPipeFile FileType = "pipe"
|
|
|
|
SymbolicLinkFile FileType = "symlink"
|
|
|
|
SocketFile FileType = "socket"
|
2024-03-20 19:23:31 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var ErrInvalidResourceURI error = errors.New("Invalid resource URI")
|
|
|
|
|
2024-03-20 16:15:27 +00:00
|
|
|
func init() {
|
2024-03-25 20:31:06 +00:00
|
|
|
ResourceTypes.Register("file", func(u *url.URL) Resource {
|
|
|
|
f := NewFile()
|
|
|
|
f.Path = filepath.Join(u.Hostname(), u.Path)
|
|
|
|
return f
|
|
|
|
})
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
2024-03-22 17:39:06 +00:00
|
|
|
// Manage the state of file system objects
|
2024-03-20 16:15:27 +00:00
|
|
|
type File struct {
|
2024-03-25 20:31:06 +00:00
|
|
|
loader YamlLoader
|
2024-04-03 16:54:50 +00:00
|
|
|
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"`
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewFile() *File {
|
2024-04-03 16:54:50 +00:00
|
|
|
currentUser, _ := user.Current()
|
|
|
|
group, _ := user.LookupGroupId(currentUser.Gid)
|
|
|
|
return &File{loader: YamlLoadDecl, Owner: currentUser.Username, Group: group.Name, Mode: "0666", FileType: RegularFile}
|
2024-03-20 19:23:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *File) URI() string {
|
2024-03-25 20:31:06 +00:00
|
|
|
return fmt.Sprintf("file://%s", f.Path)
|
2024-03-20 19:23:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *File) SetURI(uri string) error {
|
2024-03-25 20:31:06 +00:00
|
|
|
resourceUri, e := url.Parse(uri)
|
2024-04-03 19:27:16 +00:00
|
|
|
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)
|
|
|
|
}
|
2024-03-25 20:31:06 +00:00
|
|
|
}
|
|
|
|
return e
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *File) Apply() error {
|
2024-03-25 20:31:06 +00:00
|
|
|
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 {
|
2024-04-03 16:54:50 +00:00
|
|
|
case SymbolicLinkFile:
|
|
|
|
linkErr := os.Symlink(f.Target, f.Path)
|
|
|
|
if linkErr != nil {
|
|
|
|
return linkErr
|
|
|
|
}
|
2024-03-25 20:31:06 +00:00
|
|
|
case DirectoryFile:
|
2024-04-03 19:27:16 +00:00
|
|
|
if mkdirErr := os.MkdirAll(f.Path, os.FileMode(mode)); mkdirErr != nil {
|
|
|
|
return mkdirErr
|
|
|
|
}
|
2024-03-25 20:31:06 +00:00
|
|
|
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
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *File) LoadDecl(yamlFileResourceDeclaration string) error {
|
2024-03-25 20:31:06 +00:00
|
|
|
return f.loader(yamlFileResourceDeclaration, f)
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
2024-03-20 19:23:31 +00:00
|
|
|
func (f *File) ResolveId(ctx context.Context) string {
|
2024-03-25 20:31:06 +00:00
|
|
|
filePath, fileAbsErr := filepath.Abs(f.Path)
|
|
|
|
if fileAbsErr != nil {
|
|
|
|
panic(fileAbsErr)
|
|
|
|
}
|
|
|
|
f.Path = filePath
|
|
|
|
return filePath
|
2024-03-20 19:23:31 +00:00
|
|
|
}
|
|
|
|
|
2024-04-03 16:54:50 +00:00
|
|
|
func (f *File) NormalizePath() error {
|
2024-03-25 20:31:06 +00:00
|
|
|
filePath, fileAbsErr := filepath.Abs(f.Path)
|
2024-04-03 16:54:50 +00:00
|
|
|
if fileAbsErr == nil {
|
|
|
|
f.Path = filePath
|
2024-03-25 20:31:06 +00:00
|
|
|
}
|
2024-04-03 16:54:50 +00:00
|
|
|
return fileAbsErr
|
|
|
|
}
|
2024-03-25 20:31:06 +00:00
|
|
|
|
2024-04-03 16:54:50 +00:00
|
|
|
func (f *File) ReadStat() error {
|
|
|
|
info, e := os.Lstat(f.Path)
|
2024-03-25 20:31:06 +00:00
|
|
|
if e != nil {
|
|
|
|
f.State = "absent"
|
2024-04-03 16:54:50 +00:00
|
|
|
return e
|
2024-03-25 20:31:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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())
|
2024-04-03 16:54:50 +00:00
|
|
|
f.FileType.SetMode(info.Mode())
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *File) Read(ctx context.Context) ([]byte, error) {
|
2024-04-03 19:27:16 +00:00
|
|
|
if normalizePathErr := f.NormalizePath(); normalizePathErr != nil {
|
|
|
|
return nil, normalizePathErr
|
|
|
|
}
|
|
|
|
|
2024-04-03 16:54:50 +00:00
|
|
|
statErr := f.ReadStat()
|
|
|
|
if statErr != nil {
|
|
|
|
return nil, statErr
|
2024-03-25 20:31:06 +00:00
|
|
|
}
|
|
|
|
|
2024-04-03 16:54:50 +00:00
|
|
|
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
|
2024-03-25 20:31:06 +00:00
|
|
|
}
|
|
|
|
f.State = "present"
|
|
|
|
return yaml.Marshal(f)
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
2024-03-20 19:23:31 +00:00
|
|
|
|
|
|
|
func (f *File) Type() string { return "file" }
|
|
|
|
|
|
|
|
func (f *FileType) UnmarshalYAML(value *yaml.Node) error {
|
2024-03-25 20:31:06 +00:00
|
|
|
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")
|
|
|
|
}
|
2024-03-20 19:23:31 +00:00
|
|
|
}
|
2024-04-03 16:54:50 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|