package gonf import ( "bytes" "crypto/sha256" "errors" "fmt" "io" "io/fs" "log/slog" "os" "path/filepath" ) var files []*FilePromise func init() { files = make([]*FilePromise, 0) } type FileType int const ( FILE = iota DIRECTORY ) type FilePromise struct { chain []Promise contents Value dirPermissions *Permissions err error filename Value fileType FileType permissions *Permissions status Status } func Directory(filename any) *FilePromise { return &FilePromise{ chain: nil, contents: nil, dirPermissions: nil, err: nil, filename: interfaceToTemplateValue(filename), fileType: DIRECTORY, permissions: nil, status: DECLARED, } } func File(filename any) *FilePromise { return &FilePromise{ chain: nil, contents: nil, dirPermissions: nil, err: nil, filename: interfaceToTemplateValue(filename), fileType: FILE, permissions: nil, status: DECLARED, } } func (f *FilePromise) Contents(contents any) *FilePromise { f.contents = interfaceToValue(contents) return f } func (f *FilePromise) DirectoriesPermissions(p *Permissions) *FilePromise { f.dirPermissions = p return f } func (f *FilePromise) Permissions(p *Permissions) *FilePromise { f.permissions = p return f } func (f *FilePromise) Template(contents any) *FilePromise { f.contents = interfaceToTemplateValue(contents) return f } func (f *FilePromise) IfRepaired(p ...Promise) Promise { f.chain = append(f.chain, p...) return f } func (f *FilePromise) Promise() *FilePromise { if f.status == DECLARED { f.status = PROMISED files = append(files, f) } return f } func (f *FilePromise) Resolve() { filename := f.filename.String() if f.status, f.err = makeDirectoriesHierarchy(filepath.Dir(filename), f.dirPermissions); f.err != nil { return } switch f.fileType { case DIRECTORY: if f.status, f.err = makeDirectoriesHierarchy(filepath.Clean(filename), f.dirPermissions); f.err != nil { return } case FILE: if f.contents != nil { var sumFile []byte sumFile, f.err = sha256sumOfFile(filename) if f.err != nil { if !errors.Is(f.err, fs.ErrNotExist) { slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err) f.status = BROKEN return } } contents := f.contents.Bytes() sumContents := sha256sum(contents) if !bytes.Equal(sumFile, sumContents) { if f.err = writeFile(filename, contents); f.err != nil { f.status = BROKEN slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err) return } f.status = REPAIRED } } default: panic(fmt.Errorf("unknown File type enum value %d", f.fileType)) } if f.permissions != nil { var status Status status, f.err = f.permissions.resolve(filename) if f.status == PROMISED || status == BROKEN { f.status = status } if f.err != nil { slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err) return } } if f.status == REPAIRED { slog.Info("file", "filename", f.filename, "status", f.status) for _, p := range f.chain { p.Resolve() } } else { f.status = KEPT slog.Debug("file", "filename", f.filename, "status", f.status) } } func (f FilePromise) Status() Status { return f.status } func resolveFiles() (status Status) { status = KEPT for _, f := range files { if f.status == PROMISED { f.Resolve() switch f.status { case BROKEN: return BROKEN case REPAIRED: status = REPAIRED } } } return } func sha256sumOfFile(filename string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return nil, err } return h.Sum(nil), nil } func writeFile(filename string, contents []byte) error { f, err := os.Create(filename) if err != nil { return err } defer f.Close() _, err = f.Write(contents) return err }