add support for streaming the file content
This commit is contained in:
parent
69510991dc
commit
93fb0b93f0
@ -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
|
||||
if f.absPath, err = filepath.Abs(f.Path); err == nil && f.normalizePath {
|
||||
f.Path = f.absPath
|
||||
}
|
||||
return fileAbsErr
|
||||
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 nil
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user