add support for streaming the file content
Some checks are pending
Lint / golangci-lint (push) Waiting to run
Declarative Tests / test (push) Waiting to run
Declarative Tests / build-fedora (push) Waiting to run
Declarative Tests / build-ubuntu-focal (push) Waiting to run

This commit is contained in:
Matthew Rich 2024-09-19 08:06:59 +00:00
parent 69510991dc
commit 93fb0b93f0
2 changed files with 415 additions and 65 deletions

View File

@ -22,7 +22,11 @@ import (
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/iofilter" "decl/internal/iofilter"
"decl/internal/data"
"decl/internal/folio"
"decl/internal/transport"
"strings" "strings"
"embed"
) )
// Describes the type of file the resource represents // Describes the type of file the resource represents
@ -44,10 +48,22 @@ var ErrInvalidFileMode error = errors.New("Invalid Mode")
var ErrInvalidFileOwner error = errors.New("Unknown User") var ErrInvalidFileOwner error = errors.New("Unknown User")
var ErrInvalidFileGroup error = errors.New("Unknown Group") var ErrInvalidFileGroup error = errors.New("Unknown Group")
type FileMode string
func init() { func init() {
ResourceTypes.Register([]string{"file"}, func(u *url.URL) Resource { folio.DocumentRegistry.ResourceTypes.Register([]string{"file"}, func(u *url.URL) data.Resource {
f := NewFile() f := NewFile()
f.parsedURI = u
//f.Uri.SetURL(u)
f.Path = filepath.Join(u.Hostname(), u.Path) f.Path = filepath.Join(u.Hostname(), u.Path)
f.exttype, f.fileext = f.Uri.Extension()
slog.Info("folio.DocumentRegistry.ResourceTypes.Register()()", "url", u, "file", f)
/*
if absPath, err := filepath.Abs(f.Path); err == nil {
f.Filesystem = os.DirFS(filepath.Dir(absPath))
}
*/
return f return f
}) })
} }
@ -62,27 +78,36 @@ The `SerializeContent` the flag allows forcing the content to be serialized in t
*/ */
type File struct { type File struct {
Uri folio.URI `json:"uri,omitempty" yaml:"uri,omitempty"`
parsedURI *url.URL `json:"-" yaml:"-"`
Filesystem fs.FS `json:"-" yaml:"-"`
exttype string `json:"-" yaml:"-"`
fileext string `json:"-" yaml:"-"`
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
normalizePath bool `json:"-" yaml:"-"` normalizePath bool `json:"-" yaml:"-"`
absPath string `json:"-" yaml:"-"`
basePath int `json:"-" yaml:"-"`
Path string `json:"path" yaml:"path"` Path string `json:"path" yaml:"path"`
Owner string `json:"owner" yaml:"owner"` Owner string `json:"owner" yaml:"owner"`
Group string `json:"group" yaml:"group"` Group string `json:"group" yaml:"group"`
Mode string `json:"mode" yaml:"mode"` Mode FileMode `json:"mode" yaml:"mode"`
Atime time.Time `json:"atime,omitempty" yaml:"atime,omitempty"` Atime time.Time `json:"atime,omitempty" yaml:"atime,omitempty"`
Ctime time.Time `json:"ctime,omitempty" yaml:"ctime,omitempty"` Ctime time.Time `json:"ctime,omitempty" yaml:"ctime,omitempty"`
Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"` Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"`
Content string `json:"content,omitempty" yaml:"content,omitempty"` Content string `json:"content,omitempty" yaml:"content,omitempty"`
ContentSourceRef ResourceReference `json:"sourceref,omitempty" yaml:"sourceref,omitempty"` ContentSourceRef folio.ResourceReference `json:"sourceref,omitempty" yaml:"sourceref,omitempty"`
Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"` Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"`
Size int64 `json:"size,omitempty" yaml:"size,omitempty"` Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
Target string `json:"target,omitempty" yaml:"target,omitempty"` Target string `json:"target,omitempty" yaml:"target,omitempty"`
FileType FileType `json:"filetype" yaml:"filetype"` FileType FileType `json:"filetype" yaml:"filetype"`
State string `json:"state,omitempty" yaml:"state,omitempty"` State string `json:"state,omitempty" yaml:"state,omitempty"`
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"` SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
config ConfigurationValueGetter config data.ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"` Resources data.ResourceMapper `json:"-" yaml:"-"`
} }
type ResourceFileInfo struct { type ResourceFileInfo struct {
@ -103,13 +128,25 @@ func NewNormalizedFile() *File {
return f return f
} }
func (f *File) SetResourceMapper(resources ResourceMapper) { func (f *File) ContentType() string {
if f.parsedURI.Scheme != "file" {
return f.parsedURI.Scheme
}
return f.exttype
}
func (f *File) SetResourceMapper(resources data.ResourceMapper) {
f.Resources = resources f.Resources = resources
} }
func (f *File) Clone() Resource { func (f *File) Clone() data.Resource {
return &File { return &File {
Uri: f.Uri,
parsedURI: f.parsedURI,
exttype: f.exttype,
fileext: f.fileext,
normalizePath: f.normalizePath, normalizePath: f.normalizePath,
absPath: f.absPath,
Path: f.Path, Path: f.Path,
Owner: f.Owner, Owner: f.Owner,
Group: f.Group, Group: f.Group,
@ -181,26 +218,69 @@ func (f *File) Notify(m *machine.EventMessage) {
} }
} }
func (f *File) PathNormalization(flag bool) {
f.normalizePath = flag
}
func (f *File) FilePath() string {
return f.Path
}
func (f *File) SetFS(fsys fs.FS) {
f.Filesystem = fsys
}
func (f *File) URI() string { func (f *File) URI() string {
return fmt.Sprintf("file://%s", f.Path) return fmt.Sprintf("file://%s", f.Path)
} }
func (f *File) SetURI(uri string) error { func (f *File) RelativePath() string {
resourceUri, e := url.Parse(uri) return f.Path[f.basePath:]
if e == nil {
if resourceUri.Scheme == "file" {
f.Path = filepath.Join(resourceUri.Hostname(), resourceUri.Path)
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) UseConfig(config ConfigurationValueGetter) { func (f *File) SetBasePath(index int) {
if index < len(f.Path) {
f.basePath = index
}
}
func (f *File) SetURI(uri string) (err error) {
slog.Info("File.SetURI()", "uri", uri, "file", f, "parsed", f.parsedURI)
f.SetURIFromString(uri)
err = f.SetParsedURI(f.Uri.Parse())
return
}
func (f *File) SetURIFromString(uri string) {
f.Uri = folio.URI(uri)
f.exttype, f.fileext = f.Uri.Extension()
}
func (f *File) SetParsedURI(u *url.URL) (err error) {
if u != nil {
if u.Scheme == "" {
u.Scheme = "file"
f.Uri = ""
}
if f.Uri.IsEmpty() {
f.SetURIFromString(u.String())
}
slog.Info("File.SetParsedURI()", "parsed", u, "path", f.Path)
f.parsedURI = u
if f.parsedURI.Scheme == "file" {
f.Path = filepath.Join(f.parsedURI.Hostname(), f.parsedURI.Path)
slog.Info("File.SetParsedURI()", "path", f.Path)
if err = f.NormalizePath(); err != nil {
return
}
return
}
}
err = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, f.Uri)
return
}
func (f *File) UseConfig(config data.ConfigurationValueGetter) {
f.config = config f.config = config
} }
@ -229,15 +309,34 @@ func (f *File) Apply() error {
return nil return nil
} }
func (f *File) LoadDecl(yamlResourceDeclaration string) (err error) { func (f *File) Load(docData []byte, format codec.Format) (err error) {
d := codec.NewYAMLStringDecoder(yamlResourceDeclaration) err = format.StringDecoder(string(docData)).Decode(f)
err = d.Decode(f)
if err == nil { if err == nil {
f.UpdateContentAttributes() f.UpdateContentAttributes()
} }
return return
} }
func (f *File) LoadReader(r io.ReadCloser, format codec.Format) (err error) {
err = format.Decoder(r).Decode(f)
if err == nil {
f.UpdateContentAttributes()
}
return
}
func (f *File) LoadString(docData string, format codec.Format) (err error) {
err = format.StringDecoder(docData).Decode(f)
if err == nil {
f.UpdateContentAttributes()
}
return
}
func (f *File) LoadDecl(yamlResourceDeclaration string) (err error) {
return f.LoadString(yamlResourceDeclaration, codec.FormatYaml)
}
func (f *File) ResolveId(ctx context.Context) string { func (f *File) ResolveId(ctx context.Context) string {
if e := f.NormalizePath(); e != nil { if e := f.NormalizePath(); e != nil {
panic(e) panic(e)
@ -245,20 +344,34 @@ func (f *File) ResolveId(ctx context.Context) string {
return f.Path return f.Path
} }
func (f *File) NormalizePath() error { func (f *File) NormalizePath() (err error) {
if f.config != nil { if f.config != nil {
if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil { if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil {
f.Path = filepath.Join(prefixPath.(string), f.Path) f.Path = filepath.Join(prefixPath.(string), f.Path)
} }
} }
if f.normalizePath { if f.absPath, err = filepath.Abs(f.Path); err == nil && f.normalizePath {
filePath, fileAbsErr := filepath.Abs(f.Path) f.Path = f.absPath
if fileAbsErr == nil {
f.Path = filePath
}
return fileAbsErr
} }
return nil return
}
func (f *File) GetContentSourceRef() string {
return string(f.ContentSourceRef)
}
func (f *File) SetContentSourceRef(uri string) {
f.Size = 0
f.ContentSourceRef = folio.ResourceReference(uri)
}
func (f *File) Stat() (info fs.FileInfo, err error) {
if _, ok := f.Filesystem.(embed.FS); ok {
info, err = fs.Stat(f.Filesystem, f.Path)
} else {
info, err = os.Lstat(f.absPath)
}
return
} }
func (f *File) FileInfo() fs.FileInfo { func (f *File) FileInfo() fs.FileInfo {
@ -267,7 +380,7 @@ func (f *File) FileInfo() fs.FileInfo {
func (f *ResourceFileInfo) Name() string { func (f *ResourceFileInfo) Name() string {
// return filepath.Base(f.resource.Path) // return filepath.Base(f.resource.Path)
return f.resource.Path return f.resource.RelativePath()
} }
func (f *ResourceFileInfo) Size() int64 { func (f *ResourceFileInfo) Size() int64 {
@ -275,8 +388,8 @@ func (f *ResourceFileInfo) Size() int64 {
} }
func (f *ResourceFileInfo) Mode() (mode os.FileMode) { func (f *ResourceFileInfo) Mode() (mode os.FileMode) {
if fileMode, fileModeErr := strconv.ParseInt(f.resource.Mode, 8, 64); fileModeErr == nil { if fileMode, fileModeErr := f.resource.Mode.GetMode(); fileModeErr == nil {
mode |= os.FileMode(fileMode) mode |= fileMode
} else { } else {
panic(fileModeErr) panic(fileModeErr)
} }
@ -306,12 +419,12 @@ func (f *File) Create(ctx context.Context) error {
if gidErr != nil { if gidErr != nil {
return gidErr return gidErr
} }
mode, modeErr := strconv.ParseInt(f.Mode, 8, 64)
mode, modeErr := f.Mode.GetMode()
if modeErr != nil { if modeErr != nil {
return fmt.Errorf("%w: invalid mode %d", ErrInvalidFileMode, mode) return modeErr
} }
//e := os.Stat(f.path)
//if os.IsNotExist(e) {
switch f.FileType { switch f.FileType {
case SymbolicLinkFile: case SymbolicLinkFile:
linkErr := os.Symlink(f.Target, f.Path) linkErr := os.Symlink(f.Target, f.Path)
@ -319,7 +432,7 @@ func (f *File) Create(ctx context.Context) error {
return linkErr return linkErr
} }
case DirectoryFile: case DirectoryFile:
if mkdirErr := os.MkdirAll(f.Path, os.FileMode(mode)); mkdirErr != nil { if mkdirErr := os.MkdirAll(f.Path, mode); mkdirErr != nil {
return mkdirErr return mkdirErr
} }
default: default:
@ -351,19 +464,13 @@ func (f *File) Create(ctx context.Context) error {
return e return e
} }
defer createdFile.Close() defer createdFile.Close()
if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil { if chmodErr := createdFile.Chmod(mode); chmodErr != nil {
return chmodErr return chmodErr
} }
_, writeErr := io.CopyBuffer(createdFile, sumReadData, copyBuffer) _, writeErr := io.CopyBuffer(createdFile, sumReadData, copyBuffer)
if writeErr != nil { if writeErr != nil {
return fmt.Errorf("File.Create(): CopyBuffer failed %v %v: %w", createdFile, contentReader, writeErr) return fmt.Errorf("File.Create(): CopyBuffer failed %v %v: %w", createdFile, contentReader, writeErr)
} }
/*
_, writeErr := createdFile.Write([]byte(f.Content))
if writeErr != nil {
return writeErr
}
*/
f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil)) f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil))
if !f.Mtime.IsZero() && !f.Atime.IsZero() { if !f.Mtime.IsZero() && !f.Atime.IsZero() {
@ -380,6 +487,10 @@ func (f *File) Create(ctx context.Context) error {
return nil return nil
} }
func (f *File) Update(ctx context.Context) error {
return f.Create(ctx)
}
func (f *File) Delete(ctx context.Context) error { func (f *File) Delete(ctx context.Context) error {
return os.Remove(f.Path) return os.Remove(f.Path)
} }
@ -389,6 +500,10 @@ func (f *File) UpdateContentAttributes() {
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content))) f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content)))
} }
func (f *File) SetFileInfo(info os.FileInfo) error {
return f.UpdateAttributesFromFileInfo(info)
}
func (f *File) UpdateAttributesFromFileInfo(info os.FileInfo) error { func (f *File) UpdateAttributesFromFileInfo(info os.FileInfo) error {
if info != nil { if info != nil {
f.Mtime = info.ModTime() f.Mtime = info.ModTime()
@ -414,21 +529,61 @@ func (f *File) UpdateAttributesFromFileInfo(info os.FileInfo) error {
} }
} }
f.Size = info.Size() f.Size = info.Size()
f.Mode = fmt.Sprintf("%04o", info.Mode().Perm()) f.Mode = FileMode(fmt.Sprintf("%04o", info.Mode().Perm()))
f.FileType.SetMode(info.Mode()) f.FileType.SetMode(info.Mode())
return nil return nil
} }
return ErrInvalidFileInfo return ErrInvalidFileInfo
} }
func (f *File) ReadStat() error {
info, e := os.Lstat(f.Path) func (f *File) ContentSourceRefStat() (info fs.FileInfo) {
if e != nil { if len(f.ContentSourceRef) > 0 {
f.State = "absent" rs, _ := f.ContentReaderStream()
return e info, _ = rs.Stat()
rs.Close()
}
return
}
func (f *File) ReadStat() (err error) {
var info fs.FileInfo
slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Path)
info, err = f.Stat()
slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Path, "info", info, "error", err)
if err == nil {
_ = f.SetFileInfo(info)
} else {
if refStat := f.ContentSourceRefStat(); refStat != nil {
_ = f.SetFileInfo(refStat)
//f.Size = refStat.Size()
err = nil
}
// XXX compare the mod times and set state to outdated
} }
return f.UpdateAttributesFromFileInfo(info)
slog.Info("ReadStat()", "stat", info, "path", f.Path)
if err != nil {
f.State = "absent"
return
}
return
}
func (f *File) open() (file fs.File, err error) {
slog.Info("open()", "file", f.Path, "fs", f.Filesystem)
if _, ok := f.Filesystem.(embed.FS); ok {
file, err = f.Filesystem.Open(f.Path)
} else {
file, err = os.Open(f.Path)
}
slog.Info("open()", "file", f.Path, "error", err)
return
} }
func (f *File) Read(ctx context.Context) ([]byte, error) { func (f *File) Read(ctx context.Context) ([]byte, error) {
@ -444,7 +599,8 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
switch f.FileType { switch f.FileType {
case RegularFile: case RegularFile:
if len(f.ContentSourceRef) == 0 || f.SerializeContent { if len(f.ContentSourceRef) == 0 || f.SerializeContent {
file, fileErr := os.Open(f.Path) //file, fileErr := os.Open(f.Path)
file, fileErr := f.open()
if fileErr != nil { if fileErr != nil {
panic(fileErr) panic(fileErr)
} }
@ -467,13 +623,85 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
return yaml.Marshal(f) return yaml.Marshal(f)
} }
// set up reader for source content
func (f *File) readThru() (contentReader io.ReadCloser, err error) {
if len(f.ContentSourceRef) != 0 {
contentReader, err = f.ContentSourceRef.Lookup(nil).ContentReaderStream()
contentReader.(*transport.Reader).SetGzip(false)
slog.Info("File.readThru()", "reader", contentReader)
} else {
if len(f.Content) != 0 {
contentReader = io.NopCloser(strings.NewReader(f.Content))
} else {
//contentReader, err = os.Open(f.Path)
contentReader, err = f.open()
}
}
contentReader = f.UpdateContentAttributesFromReader(contentReader)
return
}
func (f *File) UpdateContentAttributesFromReader(reader io.ReadCloser) io.ReadCloser {
var content strings.Builder
hash := sha256.New()
f.Size = 0
f.Content = ""
f.Sha256 = ""
return iofilter.NewReader(reader, func(p []byte, readn int, readerr error) (n int, err error) {
hash.Write(p[:readn])
f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil))
f.Size += int64(readn)
if len(f.ContentSourceRef) == 0 || f.SerializeContent {
content.Write(p[:readn])
f.Content = content.String()
}
return readn, readerr
})
}
func (f *File) SetContent(r io.Reader) error { func (f *File) SetContent(r io.Reader) error {
fileContent, ioErr := io.ReadAll(r) fileContent, ioErr := io.ReadAll(r)
f.Content = string(fileContent) f.Content = string(fileContent)
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent)) f.UpdateContentAttributes()
return ioErr return ioErr
} }
func (f *File) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) {
slog.Info("File.GetContent()", "content", len(f.Content), "sourceref", f.ContentSourceRef)
switch f.FileType {
case RegularFile:
contentReader, err = f.readThru()
if w != nil {
copyBuffer := make([]byte, 32 * 1024)
_, writeErr := io.CopyBuffer(w, contentReader, copyBuffer)
if writeErr != nil {
return nil, fmt.Errorf("File.GetContent(): CopyBuffer failed %v %v: %w", w, contentReader, writeErr)
}
return nil, nil
}
}
return
}
func (f *File) ContentReaderStream() (*transport.Reader, error) {
if len(f.Content) == 0 && len(f.ContentSourceRef) != 0 {
return f.ContentSourceRef.Lookup(nil).ContentReaderStream()
}
return nil, fmt.Errorf("Cannot provide transport reader for string content")
}
// ContentWriterStream() would not provide a mechanism to keep the in-memory state up-to-date
func (f *File) ContentWriterStream() (*transport.Writer, error) {
if len(f.Content) == 0 && len(f.ContentSourceRef) != 0 {
return f.ContentSourceRef.Lookup(nil).ContentWriterStream()
}
return nil, fmt.Errorf("Cannot provide transport writer for string content")
}
func (f *File) GetTarget() string { return f.Target }
func (f *File) Type() string { return "file" } func (f *File) Type() string { return "file" }
func (f *FileType) UnmarshalYAML(value *yaml.Node) error { func (f *FileType) UnmarshalYAML(value *yaml.Node) error {
@ -528,3 +756,11 @@ func (f *FileType) GetMode() (mode os.FileMode) {
} }
return return
} }
func (f *FileMode) GetMode() (os.FileMode, error) {
if mode, modeErr := strconv.ParseInt(string(*f), 8, 64); modeErr != nil {
return os.FileMode(mode), fmt.Errorf("%w: %s invalid mode %d - %w", ErrInvalidFileMode, *f, mode, modeErr)
} else {
return os.FileMode(mode), nil
}
}

View File

@ -8,19 +8,22 @@ import (
"fmt" "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
_ "io" "io"
_ "log" _ "log"
_ "net/http" _ "net/http"
_ "net/http/httptest" _ "net/http/httptest"
_ "net/url" _ "net/url"
"os" "os"
"path/filepath" "path/filepath"
_ "strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
"os/user" "os/user"
"io/fs" "io/fs"
"decl/internal/codec"
"decl/internal/data"
"log/slog"
) )
func TestNewFileResource(t *testing.T) { func TestNewFileResource(t *testing.T) {
@ -101,12 +104,29 @@ func TestReadFile(t *testing.T) {
assert.YAMLEq(t, expected, string(r)) assert.YAMLEq(t, expected, string(r))
} }
func TestUseConfig(t *testing.T) {
file, _ := filepath.Abs(filepath.Join(TempDir, "missingfile.txt"))
f := NewFile()
assert.NotNil(t, f)
f.UseConfig(MockConfig(func(key string) (any, error) {
if key == "prefix" {
return "/tmp", nil
}
return nil, data.ErrUnknownConfigurationKey
}))
assert.Nil(t, f.SetURI(fmt.Sprintf("file://%s", file)))
assert.Equal(t, filepath.Join("/tmp", file), f.FilePath())
}
func TestReadFileError(t *testing.T) { func TestReadFileError(t *testing.T) {
ctx := context.Background() ctx := context.Background()
file, _ := filepath.Abs(filepath.Join(TempDir, "missingfile.txt")) file, _ := filepath.Abs(filepath.Join(TempDir, "missingfile.txt"))
f := NewFile() f := NewFile()
assert.NotEqual(t, nil, f) assert.NotNil(t, f)
f.Path = file f.Path = file
_, e := f.Read(ctx) _, e := f.Read(ctx)
assert.ErrorIs(t, e, fs.ErrNotExist) assert.ErrorIs(t, e, fs.ErrNotExist)
@ -129,7 +149,7 @@ func TestCreateFile(t *testing.T) {
f := NewFile() f := NewFile()
e := f.LoadDecl(decl) e := f.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Nil(t, e)
assert.Equal(t, ProcessTestUserName, f.Owner) assert.Equal(t, ProcessTestUserName, f.Owner)
applyErr := f.Apply() applyErr := f.Apply()
@ -141,10 +161,44 @@ func TestCreateFile(t *testing.T) {
assert.Greater(t, s.Size(), int64(0)) assert.Greater(t, s.Size(), int64(0))
f.State = "absent" f.State = "absent"
assert.Equal(t, nil, f.Apply()) assert.Nil(t, f.Apply())
assert.NoFileExists(t, file, nil) assert.NoFileExists(t, file, nil)
} }
func TestLoadFile(t *testing.T) {
file, _ := filepath.Abs(filepath.Join(TempDir, "foo.txt"))
decl := fmt.Sprintf(`
path: "%s"
owner: "%s"
group: "%s"
mode: "0600"
content: |-
test line 1
test line 2
state: present
`, file, ProcessTestUserName, ProcessTestGroupName)
f := NewFile()
assert.Nil(t, f.Load([]byte(decl), codec.FormatYaml))
assert.Equal(t, ProcessTestUserName, f.Owner)
assert.Greater(t, f.Size, int64(0))
reader := io.NopCloser(strings.NewReader(decl))
fr := NewFile()
assert.Nil(t, fr.LoadReader(reader, codec.FormatYaml))
assert.Equal(t, ProcessTestUserName, f.Owner)
assert.Greater(t, f.Size, int64(0))
contentReaderTransport, trErr := fr.ContentReaderStream()
assert.ErrorContains(t, trErr, "Cannot provide transport reader for string content")
assert.Nil(t, contentReaderTransport)
contentWriterTransport, trErr := fr.ContentWriterStream()
assert.ErrorContains(t, trErr, "Cannot provide transport writer for string content")
assert.Nil(t, contentWriterTransport)
}
func TestFileType(t *testing.T) { func TestFileType(t *testing.T) {
fileType := []byte(` fileType := []byte(`
filetype: "directory" filetype: "directory"
@ -240,6 +294,9 @@ func TestFileUpdateAttributesFromFileInfo(t *testing.T) {
updateAttributesErr := f.UpdateAttributesFromFileInfo(info) updateAttributesErr := f.UpdateAttributesFromFileInfo(info)
assert.Nil(t, updateAttributesErr) assert.Nil(t, updateAttributesErr)
assert.Equal(t, DirectoryFile, f.FileType) assert.Equal(t, DirectoryFile, f.FileType)
assert.ErrorIs(t, f.SetFileInfo(nil), ErrInvalidFileInfo)
} }
func TestFileReadStat(t *testing.T) { func TestFileReadStat(t *testing.T) {
@ -267,11 +324,14 @@ func TestFileReadStat(t *testing.T) {
l := NewFile() l := NewFile()
assert.NotNil(t, l) assert.NotNil(t, l)
assert.Nil(t, l.NormalizePath())
l.FileType = SymbolicLinkFile l.FileType = SymbolicLinkFile
l.Path = link l.Path = link
l.Target = linkTargetFile l.Target = linkTargetFile
l.State = "present" l.State = "present"
slog.Info("TestFileReadStat()", "file", f, "link", l)
applyErr := l.Apply() applyErr := l.Apply()
assert.Nil(t, applyErr) assert.Nil(t, applyErr)
readStatErr := l.ReadStat() readStatErr := l.ReadStat()
@ -292,6 +352,7 @@ func TestFileResourceFileInfo(t *testing.T) {
f.Path = testFile f.Path = testFile
f.Mode = "0600" f.Mode = "0600"
f.Content = "some test data"
f.State = "present" f.State = "present"
assert.Nil(t, f.Apply()) assert.Nil(t, f.Apply())
@ -300,6 +361,11 @@ func TestFileResourceFileInfo(t *testing.T) {
fi := f.FileInfo() fi := f.FileInfo()
assert.Equal(t, os.FileMode(0600), fi.Mode().Perm()) assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
assert.Equal(t, testFile, fi.Name())
assert.False(t, fi.IsDir())
assert.Nil(t, fi.Sys())
assert.Greater(t, time.Now(), fi.ModTime())
assert.Greater(t, fi.Size(), int64(0))
} }
func TestFileClone(t *testing.T) { func TestFileClone(t *testing.T) {
@ -352,13 +418,13 @@ func TestFileErrors(t *testing.T) {
readStater := read.StateMachine() readStater := read.StateMachine()
read.Path = testFile read.Path = testFile
assert.Nil(t, readStater.Trigger("read")) assert.Nil(t, readStater.Trigger("read"))
assert.Equal(t, "0631", read.Mode) assert.Equal(t, FileMode("0631"), read.Mode)
f.Mode = "900" f.Mode = "900"
assert.ErrorAs(t, stater.Trigger("create"), &ErrInvalidFileMode, "Apply should fail with NumError when converting invalid octal") assert.ErrorAs(t, stater.Trigger("create"), &ErrInvalidFileMode, "Apply should fail with NumError when converting invalid octal")
assert.Nil(t, readStater.Trigger("read")) assert.Nil(t, readStater.Trigger("read"))
assert.Equal(t, "0631", read.Mode) assert.Equal(t, FileMode("0631"), read.Mode)
f.Mode = "0631" f.Mode = "0631"
f.Owner = "bar" f.Owner = "bar"
@ -466,7 +532,7 @@ func TestFilePathURI(t *testing.T) {
f := NewFile() f := NewFile()
e := f.LoadDecl(decl) e := f.LoadDecl(decl)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, "", f.Path) assert.Equal(t, "", f.FilePath())
assert.ErrorContains(t, f.Validate(), "path: String length must be greater than or equal to 1") assert.ErrorContains(t, f.Validate(), "path: String length must be greater than or equal to 1")
} }
@ -484,7 +550,7 @@ func TestFileAbsent(t *testing.T) {
f := NewFile() f := NewFile()
stater := f.StateMachine() stater := f.StateMachine()
e := f.LoadDecl(decl) e := f.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Nil(t, e)
assert.Equal(t, ProcessTestUserName, f.Owner) assert.Equal(t, ProcessTestUserName, f.Owner)
err := stater.Trigger("read") err := stater.Trigger("read")
@ -493,3 +559,51 @@ func TestFileAbsent(t *testing.T) {
assert.Equal(t, "absent", f.State) assert.Equal(t, "absent", f.State)
} }
func TestFileReader(t *testing.T) {
expected := "datatoreadusinganio.readers"
dataReader := strings.NewReader(expected)
file, _ := filepath.Abs(filepath.Join(TempDir, "testabsentstate.txt"))
decl := fmt.Sprintf(`
path: "%s"
owner: "%s"
group: "%s"
mode: "0600"
content: "%s"
filetype: "regular"
`, file, ProcessTestUserName, ProcessTestGroupName, expected)
f := NewFile()
assert.Nil(t, f.LoadString(decl, codec.FormatYaml))
assert.Nil(t, f.SetContent(dataReader))
reader, err := f.GetContent(nil)
assert.Nil(t, err)
value, ioErr := io.ReadAll(reader)
assert.Nil(t, ioErr)
assert.Equal(t, expected, string(value))
var writer strings.Builder
_, writerErr := f.GetContent(&writer)
assert.Nil(t, writerErr)
assert.Equal(t, expected, writer.String())
}
func TestFileSetURIError(t *testing.T) {
file := fmt.Sprintf("%s/%s", TempDir, "fooread.txt")
f := NewFile()
assert.NotNil(t, f)
e := f.SetURI("foo://" + file)
assert.NotNil(t, e)
assert.ErrorIs(t, e, ErrInvalidResourceURI)
}
func TestFileContentType(t *testing.T) {
file := fmt.Sprintf("%s/%s", TempDir, "fooread.txt")
f := NewFile()
assert.NotNil(t, f)
e := f.SetURI("file://" + file)
assert.Nil(t, e)
assert.Equal(t, "txt", f.ContentType())
}