jx/internal/resource/file.go
Matthew Rich 4c1540685d
Some checks failed
Lint / golangci-lint (push) Failing after 9m49s
Declarative Tests / test (push) Successful in 1m19s
fix generating shasum for tar sourced files. move enc/decoders to separate pkg
2024-05-14 11:26:05 -07:00

430 lines
9.8 KiB
Go

// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"errors"
"fmt"
"log/slog"
"gopkg.in/yaml.v3"
"io"
"io/fs"
"net/url"
"os"
"os/user"
"path/filepath"
"strconv"
"syscall"
"time"
"crypto/sha256"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
)
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")
var ErrInvalidFileInfo error = errors.New("Invalid FileInfo")
var ErrInvalidFileMode error = errors.New("Invalid Mode")
var ErrInvalidFileOwner error = errors.New("Unknown User")
var ErrInvalidFileGroup error = errors.New("Unknown Group")
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 {
stater machine.Stater `json:"-" yaml:"-"`
normalizePath bool `json:"-" yaml:"-"`
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"`
Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"`
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
Target string `json:"target,omitempty" yaml:"target,omitempty"`
FileType FileType `json:"filetype" yaml:"filetype"`
State string `json:"state,omitempty" yaml:"state,omitempty"`
}
type ResourceFileInfo struct {
resource *File
}
func NewFile() *File {
currentUser, _ := user.Current()
group, _ := user.LookupGroupId(currentUser.Gid)
f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile}
slog.Info("NewFile()", "file", f)
return f
}
func NewNormalizedFile() *File {
f := NewFile()
f.normalizePath = true
return f
}
func (f *File) Clone() Resource {
return &File {
normalizePath: f.normalizePath,
Path: f.Path,
Owner: f.Owner,
Group: f.Group,
Mode: f.Mode,
Atime: f.Atime,
Ctime: f.Ctime,
Mtime: f.Mtime,
Content: f.Content,
Sha256: f.Sha256,
Size: f.Size,
Target: f.Target,
FileType: f.FileType,
State: f.State,
}
}
func (f *File) StateMachine() machine.Stater {
if f.stater == nil {
f.stater = StorageMachine(f)
}
return f.stater
}
func (f *File) Notify(m *machine.EventMessage) {
ctx := context.Background()
slog.Info("Notify()", "file", f, "m", m)
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_create":
if e := f.Create(ctx); e == nil {
if triggerErr := f.stater.Trigger("created"); triggerErr == nil {
return
}
}
f.State = "absent"
case "present":
f.State = "present"
}
case machine.EXITSTATEEVENT:
}
}
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 = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())
if err := f.NormalizePath(); err != nil {
return err
}
} 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":
return f.Create(context.Background())
}
return nil
}
func (f *File) LoadDecl(yamlResourceDeclaration string) (err error) {
d := codec.NewYAMLStringDecoder(yamlResourceDeclaration)
err = d.Decode(f)
if err == nil {
f.UpdateContentAttributes()
}
return
}
func (f *File) ResolveId(ctx context.Context) string {
if e := f.NormalizePath(); e != nil {
panic(e)
}
return f.Path
}
func (f *File) NormalizePath() error {
if f.normalizePath {
filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr == nil {
f.Path = filePath
}
return fileAbsErr
}
return nil
}
func (f *File) FileInfo() fs.FileInfo {
return &ResourceFileInfo{ resource: f }
}
func (f *ResourceFileInfo) Name() string {
// return filepath.Base(f.resource.Path)
return f.resource.Path
}
func (f *ResourceFileInfo) Size() int64 {
return f.resource.Size
}
func (f *ResourceFileInfo) Mode() (mode os.FileMode) {
if fileMode, fileModeErr := strconv.ParseInt(f.resource.Mode, 8, 64); fileModeErr == nil {
mode |= os.FileMode(fileMode)
} else {
panic(fileModeErr)
}
mode |= f.resource.FileType.GetMode()
return
}
func (f *ResourceFileInfo) ModTime() time.Time {
return f.resource.Mtime
}
func (f *ResourceFileInfo) IsDir() bool {
return f.resource.FileType == DirectoryFile
}
func (f *ResourceFileInfo) Sys() any {
return nil
}
func (f *File) Create(ctx context.Context) error {
uid, uidErr := LookupUID(f.Owner)
if uidErr != nil {
return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid)
}
gid, gidErr := LookupGID(f.Group)
if gidErr != nil {
return gidErr
}
mode, modeErr := strconv.ParseInt(f.Mode, 8, 64)
if modeErr != nil {
return fmt.Errorf("%w: invalid mode %d", ErrInvalidFileMode, mode)
}
//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
}
f.State = "present"
return nil
}
func (f *File) UpdateContentAttributes() {
f.Size = int64(len(f.Content))
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content)))
}
func (f *File) UpdateAttributesFromFileInfo(info os.FileInfo) error {
if info != nil {
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.Username
}
fileGroup, groupErr := user.LookupGroupId(groupId)
if groupErr != nil {
//panic(groupErr)
f.Group = groupId
} else {
f.Group = fileGroup.Name
}
}
f.Size = info.Size()
f.Mode = fmt.Sprintf("%04o", info.Mode().Perm())
f.FileType.SetMode(info.Mode())
return nil
}
return ErrInvalidFileInfo
}
func (f *File) ReadStat() error {
info, e := os.Lstat(f.Path)
if e != nil {
f.State = "absent"
return e
}
return f.UpdateAttributesFromFileInfo(info)
}
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)
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(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) SetContent(r io.Reader) error {
fileContent, ioErr := io.ReadAll(r)
f.Content = string(fileContent)
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
return ioErr
}
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
}
}
func (f *FileType) GetMode() (mode os.FileMode) {
switch *f {
case RegularFile:
case DirectoryFile:
mode |= os.ModeDir
case SymbolicLinkFile:
mode |= os.ModeSymlink
case NamedPipeFile:
mode |= os.ModeNamedPipe
case SocketFile:
mode |= os.ModeSocket
case CharacterDeviceFile:
mode |= os.ModeCharDevice
case BlockDeviceFile:
mode |= os.ModeDevice
}
return
}