jx/internal/resource/file.go

458 lines
10 KiB
Go
Raw Permalink Normal View History

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"
"log/slog"
2024-03-25 20:31:06 +00:00
"gopkg.in/yaml.v3"
"io"
2024-04-23 22:35:08 +00:00
"io/fs"
2024-03-25 20:31:06 +00:00
"net/url"
"os"
"os/user"
"path/filepath"
"strconv"
"syscall"
"time"
2024-04-21 06:13:17 +00:00
"crypto/sha256"
2024-05-06 00:48:54 +00:00
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
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-04-19 07:52:10 +00:00
var ErrInvalidFileInfo error = errors.New("Invalid FileInfo")
2024-05-06 00:48:54 +00:00
var ErrInvalidFileMode error = errors.New("Invalid Mode")
var ErrInvalidFileOwner error = errors.New("Unknown User")
var ErrInvalidFileGroup error = errors.New("Unknown Group")
2024-03-20 19:23:31 +00:00
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-05-09 07:39:45 +00:00
stater machine.Stater `json:"-" yaml:"-"`
2024-04-25 07:45:05 +00:00
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"`
2024-04-21 06:13:17 +00:00
Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"`
2024-04-23 22:35:08 +00:00
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
Target string `json:"target,omitempty" yaml:"target,omitempty"`
FileType FileType `json:"filetype" yaml:"filetype"`
2024-05-06 00:48:54 +00:00
State string `json:"state,omitempty" yaml:"state,omitempty"`
2024-03-20 16:15:27 +00:00
}
2024-04-23 22:35:08 +00:00
type ResourceFileInfo struct {
resource *File
}
2024-03-20 16:15:27 +00:00
func NewFile() *File {
currentUser, _ := user.Current()
group, _ := user.LookupGroupId(currentUser.Gid)
2024-04-25 07:45:05 +00:00
f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile}
slog.Info("NewFile()", "file", f)
return f
2024-03-20 19:23:31 +00:00
}
2024-04-25 07:45:05 +00:00
func NewNormalizedFile() *File {
f := NewFile()
f.normalizePath = true
return f
}
2024-04-19 07:52:10 +00:00
func (f *File) Clone() Resource {
return &File {
2024-04-25 07:45:05 +00:00
normalizePath: f.normalizePath,
2024-04-19 07:52:10 +00:00
Path: f.Path,
Owner: f.Owner,
Group: f.Group,
Mode: f.Mode,
Atime: f.Atime,
Ctime: f.Ctime,
Mtime: f.Mtime,
Content: f.Content,
2024-04-23 22:35:08 +00:00
Sha256: f.Sha256,
Size: f.Size,
2024-04-19 07:52:10 +00:00
Target: f.Target,
FileType: f.FileType,
State: f.State,
}
}
2024-05-06 00:48:54 +00:00
func (f *File) StateMachine() machine.Stater {
2024-05-09 07:39:45 +00:00
if f.stater == nil {
f.stater = StorageMachine(f)
}
return f.stater
}
func (f *File) Notify(m *machine.EventMessage) {
ctx := context.Background()
2024-05-12 08:20:51 +00:00
slog.Info("Notify()", "file", f, "m", m)
2024-05-09 07:39:45 +00:00
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
2024-05-24 05:11:51 +00:00
case "start_read":
if _,readErr := f.Read(ctx); readErr == nil {
if triggerErr := f.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
f.State = "absent"
panic(triggerErr)
}
} else {
f.State = "absent"
panic(readErr)
}
2024-05-09 07:39:45 +00:00
case "start_create":
2024-05-13 17:13:20 +00:00
if e := f.Create(ctx); e == nil {
2024-05-24 05:11:51 +00:00
if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil {
return
}
}
f.State = "absent"
case "start_delete":
if deleteErr := f.Delete(ctx); deleteErr == nil {
if triggerErr := f.StateMachine().Trigger("deleted"); triggerErr == nil {
2024-05-13 17:13:20 +00:00
return
2024-05-24 05:11:51 +00:00
} else {
f.State = "present"
panic(triggerErr)
2024-05-13 05:41:12 +00:00
}
2024-05-24 05:11:51 +00:00
} else {
f.State = "present"
panic(deleteErr)
2024-05-09 07:39:45 +00:00
}
2024-05-24 05:11:51 +00:00
case "absent":
2024-05-13 17:13:20 +00:00
f.State = "absent"
2024-05-24 05:11:51 +00:00
case "present", "created", "read":
2024-05-09 07:39:45 +00:00
f.State = "present"
}
case machine.EXITSTATEEVENT:
}
2024-05-06 00:48:54 +00:00
}
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" {
2024-04-25 07:45:05 +00:00
f.Path = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())
if err := f.NormalizePath(); err != nil {
2024-04-21 06:13:17 +00:00
return err
}
2024-04-03 19:27:16 +00:00
} 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
}
2024-04-09 19:30:05 +00:00
func (f *File) Validate() error {
return fmt.Errorf("failed")
}
2024-03-20 16:15:27 +00:00
func (f *File) Apply() error {
2024-05-24 05:11:51 +00:00
ctx := context.Background()
2024-03-25 20:31:06 +00:00
switch f.State {
case "absent":
2024-05-24 05:11:51 +00:00
return f.Delete(ctx)
2024-03-25 20:31:06 +00:00
case "present":
2024-05-24 05:11:51 +00:00
return f.Create(ctx)
2024-03-25 20:31:06 +00:00
}
return nil
2024-03-20 16:15:27 +00:00
}
2024-04-25 07:45:05 +00:00
func (f *File) LoadDecl(yamlResourceDeclaration string) (err error) {
d := codec.NewYAMLStringDecoder(yamlResourceDeclaration)
2024-04-25 07:45:05 +00:00
err = d.Decode(f)
if err == nil {
f.UpdateContentAttributes()
}
return
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-04-25 07:45:05 +00:00
if e := f.NormalizePath(); e != nil {
panic(e)
2024-03-25 20:31:06 +00:00
}
2024-04-25 07:45:05 +00:00
return f.Path
2024-03-20 19:23:31 +00:00
}
func (f *File) NormalizePath() error {
2024-04-25 07:45:05 +00:00
if f.normalizePath {
filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr == nil {
f.Path = filePath
}
return fileAbsErr
2024-03-25 20:31:06 +00:00
}
2024-04-25 07:45:05 +00:00
return nil
}
2024-03-25 20:31:06 +00:00
2024-04-23 22:35:08 +00:00
func (f *File) FileInfo() fs.FileInfo {
return &ResourceFileInfo{ resource: f }
}
func (f *ResourceFileInfo) Name() string {
2024-04-25 07:45:05 +00:00
// return filepath.Base(f.resource.Path)
return f.resource.Path
2024-04-23 22:35:08 +00:00
}
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 {
2024-05-06 04:40:34 +00:00
return f.resource.FileType == DirectoryFile
2024-04-23 22:35:08 +00:00
}
func (f *ResourceFileInfo) Sys() any {
return nil
}
2024-05-09 07:39:45 +00:00
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
}
2024-05-12 08:20:51 +00:00
f.State = "present"
2024-05-09 07:39:45 +00:00
return nil
}
2024-05-24 05:11:51 +00:00
func (f *File) Delete(ctx context.Context) error {
return os.Remove(f.Path)
}
2024-04-25 07:45:05 +00:00
func (f *File) UpdateContentAttributes() {
f.Size = int64(len(f.Content))
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content)))
}
2024-04-19 07:52:10 +00:00
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
}
}
2024-04-23 22:35:08 +00:00
f.Size = info.Size()
2024-04-19 07:52:10 +00:00
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)
2024-03-25 20:31:06 +00:00
if e != nil {
f.State = "absent"
return e
2024-03-25 20:31:06 +00:00
}
2024-04-19 07:52:10 +00:00
return f.UpdateAttributesFromFileInfo(info)
}
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
}
statErr := f.ReadStat()
if statErr != nil {
return nil, statErr
2024-03-25 20:31:06 +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)
2024-04-21 06:13:17 +00:00
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
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) SetContent(r io.Reader) error {
fileContent, ioErr := io.ReadAll(r)
f.Content = string(fileContent)
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
return ioErr
}
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
}
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
}
}
2024-04-23 22:35:08 +00:00
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
}