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"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user