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"
"decl/internal/codec"
"decl/internal/iofilter"
"decl/internal/data"
"decl/internal/folio"
"decl/internal/transport"
"strings"
"embed"
)
// 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 ErrInvalidFileGroup error = errors.New("Unknown Group")
type FileMode string
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.parsedURI = u
//f.Uri.SetURL(u)
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
})
}
@ -62,27 +78,36 @@ The `SerializeContent` the flag allows forcing the content to be serialized in t
*/
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:"-"`
normalizePath bool `json:"-" yaml:"-"`
absPath string `json:"-" yaml:"-"`
basePath int `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"`
Mode FileMode `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"`
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"`
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"`
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
config ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"`
config data.ConfigurationValueGetter
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
type ResourceFileInfo struct {
@ -103,13 +128,25 @@ func NewNormalizedFile() *File {
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
}
func (f *File) Clone() Resource {
func (f *File) Clone() data.Resource {
return &File {
Uri: f.Uri,
parsedURI: f.parsedURI,
exttype: f.exttype,
fileext: f.fileext,
normalizePath: f.normalizePath,
absPath: f.absPath,
Path: f.Path,
Owner: f.Owner,
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 {
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.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) RelativePath() string {
return f.Path[f.basePath:]
}
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
}
@ -229,15 +309,34 @@ func (f *File) Apply() error {
return nil
}
func (f *File) LoadDecl(yamlResourceDeclaration string) (err error) {
d := codec.NewYAMLStringDecoder(yamlResourceDeclaration)
err = d.Decode(f)
func (f *File) Load(docData []byte, format codec.Format) (err error) {
err = format.StringDecoder(string(docData)).Decode(f)
if err == nil {
f.UpdateContentAttributes()
}
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 {
if e := f.NormalizePath(); e != nil {
panic(e)
@ -245,20 +344,34 @@ func (f *File) ResolveId(ctx context.Context) string {
return f.Path
}
func (f *File) NormalizePath() error {
func (f *File) NormalizePath() (err error) {
if f.config != nil {
if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil {
f.Path = filepath.Join(prefixPath.(string), f.Path)
}
}
if f.normalizePath {
filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr == nil {
f.Path = filePath
}
return fileAbsErr
if f.absPath, err = filepath.Abs(f.Path); err == nil && f.normalizePath {
f.Path = f.absPath
}
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 {
@ -267,7 +380,7 @@ func (f *File) FileInfo() fs.FileInfo {
func (f *ResourceFileInfo) Name() string {
// return filepath.Base(f.resource.Path)
return f.resource.Path
return f.resource.RelativePath()
}
func (f *ResourceFileInfo) Size() int64 {
@ -275,8 +388,8 @@ func (f *ResourceFileInfo) Size() int64 {
}
func (f *ResourceFileInfo) Mode() (mode os.FileMode) {
if fileMode, fileModeErr := strconv.ParseInt(f.resource.Mode, 8, 64); fileModeErr == nil {
mode |= os.FileMode(fileMode)
if fileMode, fileModeErr := f.resource.Mode.GetMode(); fileModeErr == nil {
mode |= fileMode
} else {
panic(fileModeErr)
}
@ -306,12 +419,12 @@ func (f *File) Create(ctx context.Context) error {
if gidErr != nil {
return gidErr
}
mode, modeErr := strconv.ParseInt(f.Mode, 8, 64)
mode, modeErr := f.Mode.GetMode()
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 {
case SymbolicLinkFile:
linkErr := os.Symlink(f.Target, f.Path)
@ -319,7 +432,7 @@ func (f *File) Create(ctx context.Context) error {
return linkErr
}
case DirectoryFile:
if mkdirErr := os.MkdirAll(f.Path, os.FileMode(mode)); mkdirErr != nil {
if mkdirErr := os.MkdirAll(f.Path, mode); mkdirErr != nil {
return mkdirErr
}
default:
@ -351,19 +464,13 @@ func (f *File) Create(ctx context.Context) error {
return e
}
defer createdFile.Close()
if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil {
if chmodErr := createdFile.Chmod(mode); chmodErr != nil {
return chmodErr
}
_, writeErr := io.CopyBuffer(createdFile, sumReadData, copyBuffer)
if writeErr != nil {
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))
if !f.Mtime.IsZero() && !f.Atime.IsZero() {
@ -380,6 +487,10 @@ func (f *File) Create(ctx context.Context) error {
return nil
}
func (f *File) Update(ctx context.Context) error {
return f.Create(ctx)
}
func (f *File) Delete(ctx context.Context) error {
return os.Remove(f.Path)
}
@ -389,6 +500,10 @@ func (f *File) UpdateContentAttributes() {
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 {
if info != nil {
f.Mtime = info.ModTime()
@ -414,21 +529,61 @@ func (f *File) UpdateAttributesFromFileInfo(info os.FileInfo) error {
}
}
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())
return nil
}
return ErrInvalidFileInfo
}
func (f *File) ReadStat() error {
info, e := os.Lstat(f.Path)
if e != nil {
f.State = "absent"
return e
func (f *File) ContentSourceRefStat() (info fs.FileInfo) {
if len(f.ContentSourceRef) > 0 {
rs, _ := f.ContentReaderStream()
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) {
@ -444,7 +599,8 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
switch f.FileType {
case RegularFile:
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 {
panic(fileErr)
}
@ -467,13 +623,85 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
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 {
fileContent, ioErr := io.ReadAll(r)
f.Content = string(fileContent)
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
f.UpdateContentAttributes()
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 *FileType) UnmarshalYAML(value *yaml.Node) error {
@ -528,3 +756,11 @@ func (f *FileType) GetMode() (mode os.FileMode) {
}
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"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
_ "io"
"io"
_ "log"
_ "net/http"
_ "net/http/httptest"
_ "net/url"
"os"
"path/filepath"
_ "strings"
"strings"
"syscall"
"testing"
"time"
"os/user"
"io/fs"
"decl/internal/codec"
"decl/internal/data"
"log/slog"
)
func TestNewFileResource(t *testing.T) {
@ -101,12 +104,29 @@ func TestReadFile(t *testing.T) {
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) {
ctx := context.Background()
file, _ := filepath.Abs(filepath.Join(TempDir, "missingfile.txt"))
f := NewFile()
assert.NotEqual(t, nil, f)
assert.NotNil(t, f)
f.Path = file
_, e := f.Read(ctx)
assert.ErrorIs(t, e, fs.ErrNotExist)
@ -129,7 +149,7 @@ func TestCreateFile(t *testing.T) {
f := NewFile()
e := f.LoadDecl(decl)
assert.Equal(t, nil, e)
assert.Nil(t, e)
assert.Equal(t, ProcessTestUserName, f.Owner)
applyErr := f.Apply()
@ -141,10 +161,44 @@ func TestCreateFile(t *testing.T) {
assert.Greater(t, s.Size(), int64(0))
f.State = "absent"
assert.Equal(t, nil, f.Apply())
assert.Nil(t, f.Apply())
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) {
fileType := []byte(`
filetype: "directory"
@ -240,6 +294,9 @@ func TestFileUpdateAttributesFromFileInfo(t *testing.T) {
updateAttributesErr := f.UpdateAttributesFromFileInfo(info)
assert.Nil(t, updateAttributesErr)
assert.Equal(t, DirectoryFile, f.FileType)
assert.ErrorIs(t, f.SetFileInfo(nil), ErrInvalidFileInfo)
}
func TestFileReadStat(t *testing.T) {
@ -267,11 +324,14 @@ func TestFileReadStat(t *testing.T) {
l := NewFile()
assert.NotNil(t, l)
assert.Nil(t, l.NormalizePath())
l.FileType = SymbolicLinkFile
l.Path = link
l.Target = linkTargetFile
l.State = "present"
slog.Info("TestFileReadStat()", "file", f, "link", l)
applyErr := l.Apply()
assert.Nil(t, applyErr)
readStatErr := l.ReadStat()
@ -292,6 +352,7 @@ func TestFileResourceFileInfo(t *testing.T) {
f.Path = testFile
f.Mode = "0600"
f.Content = "some test data"
f.State = "present"
assert.Nil(t, f.Apply())
@ -300,6 +361,11 @@ func TestFileResourceFileInfo(t *testing.T) {
fi := f.FileInfo()
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) {
@ -352,13 +418,13 @@ func TestFileErrors(t *testing.T) {
readStater := read.StateMachine()
read.Path = testFile
assert.Nil(t, readStater.Trigger("read"))
assert.Equal(t, "0631", read.Mode)
assert.Equal(t, FileMode("0631"), read.Mode)
f.Mode = "900"
assert.ErrorAs(t, stater.Trigger("create"), &ErrInvalidFileMode, "Apply should fail with NumError when converting invalid octal")
assert.Nil(t, readStater.Trigger("read"))
assert.Equal(t, "0631", read.Mode)
assert.Equal(t, FileMode("0631"), read.Mode)
f.Mode = "0631"
f.Owner = "bar"
@ -466,7 +532,7 @@ func TestFilePathURI(t *testing.T) {
f := NewFile()
e := f.LoadDecl(decl)
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")
}
@ -484,7 +550,7 @@ func TestFileAbsent(t *testing.T) {
f := NewFile()
stater := f.StateMachine()
e := f.LoadDecl(decl)
assert.Equal(t, nil, e)
assert.Nil(t, e)
assert.Equal(t, ProcessTestUserName, f.Owner)
err := stater.Trigger("read")
@ -493,3 +559,51 @@ func TestFileAbsent(t *testing.T) {
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())
}