jx/internal/resource/file.go

312 lines
7.0 KiB
Go
Raw 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"
"net/url"
"os"
"os/user"
"path/filepath"
"strconv"
"syscall"
"time"
2024-04-21 06:13:17 +00:00
"crypto/sha256"
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-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 {
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"`
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 {
currentUser, _ := user.Current()
group, _ := user.LookupGroupId(currentUser.Gid)
f := &File{Owner: currentUser.Username, Group: group.Name, Mode: "0666", FileType: RegularFile}
slog.Info("NewFile()", "file", f)
return f
2024-03-20 19:23:31 +00:00
}
2024-04-19 07:52:10 +00:00
func (f *File) Clone() Resource {
return &File {
Path: f.Path,
Owner: f.Owner,
Group: f.Group,
Mode: f.Mode,
Atime: f.Atime,
Ctime: f.Ctime,
Mtime: f.Mtime,
Content: f.Content,
Target: f.Target,
FileType: f.FileType,
State: f.State,
}
}
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-21 06:13:17 +00:00
if absFilePath, err := filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())); err != nil {
return err
} else {
f.Path = absFilePath
}
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-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 {
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(yamlResourceDeclaration string) error {
d := NewYAMLStringDecoder(yamlResourceDeclaration)
return d.Decode(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
}
func (f *File) NormalizePath() error {
2024-03-25 20:31:06 +00:00
filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr == nil {
f.Path = filePath
2024-03-25 20:31:06 +00:00
}
return fileAbsErr
}
2024-03-25 20:31:06 +00:00
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
}
}
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) 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
}
}