Skip to content

Commit

Permalink
Initial
Browse files Browse the repository at this point in the history
Signed-off-by: Paweł Gronowski <[email protected]>
  • Loading branch information
vvoland committed Nov 25, 2024
0 parents commit ae4ccb3
Show file tree
Hide file tree
Showing 4 changed files with 406 additions and 0 deletions.
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module woland.xyz/livefile

go 1.21

require (
github.com/rs/zerolog v1.33.0
gotest.tools v2.2.0+incompatible
)

require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/sys v0.12.0 // indirect
)
20 changes: 20 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
162 changes: 162 additions & 0 deletions statefile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package statefile

import (
"context"
"encoding/json"
"errors"
"io"
"os"
"path"
"path/filepath"
"sync"
"time"

"github.com/rs/zerolog/log"
)

var BaseDir string

type StateFile[StateT any] struct {
path string

lastModTime time.Time
cached StateT
mutex sync.Mutex

defaultFunc func() StateT
}

func New[T any](path string, def func() T) *StateFile[T] {
if !filepath.IsAbs(path) && BaseDir != "" {
path = filepath.Join(BaseDir, path)
}
return &StateFile[T]{
path: path,
defaultFunc: def,
cached: def(),
}
}

func (ps *StateFile[T]) Peek(ctx context.Context) T {
ps.mutex.Lock()
ps.ensure(ctx)
c := ps.cached
ps.mutex.Unlock()
return c
}

func (ps *StateFile[T]) ensure(ctx context.Context) {
file, err := os.Open(ps.path)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
panic(err)
}
} else {
ps.loadIfUpdated(ctx, file)
file.Close()
}
}

func (ps *StateFile[T]) View(ctx context.Context, f func(state *T)) {
ps.mutex.Lock()
defer ps.mutex.Unlock()

ps.ensure(ctx)
f(&ps.cached)
}

func (ps *StateFile[T]) loadIfUpdated(ctx context.Context, file *os.File) {
stat, err := file.Stat()
if err != nil {
panic(err)
}

if stat.Size() == 0 {
return
}

modTime := stat.ModTime()
if modTime.After(ps.lastModTime) {
ps.forceLoad(ctx, file)
ps.lastModTime = modTime
}
}

func (ps *StateFile[T]) forceLoad(ctx context.Context, file *os.File) {
_, err := file.Seek(0, io.SeekStart)
if err != nil {
panic(err)
}

decoder := json.NewDecoder(file)
err = decoder.Decode(&ps.cached)

// File empty
if err == io.EOF && decoder.InputOffset() == 0 {
ps.cached = ps.defaultFunc()
err = nil
}
log.Ctx(ctx).Debug().
Err(err).
Any("data", ps.cached).
Str("path", ps.path).
Time("lastMod", ps.lastModTime).
Time("timestamp", time.Now()).
Msg("loaded")

if err != nil {
panic(err)
}
}

func (ps *StateFile[T]) Update(ctx context.Context, f func(state *T) error) error {
ps.mutex.Lock()
defer ps.mutex.Unlock()

ps.ensure(ctx)

file, err := os.OpenFile(ps.path, os.O_RDWR|os.O_CREATE, 0o660)
if errors.Is(err, os.ErrNotExist) {
err = os.MkdirAll(path.Dir(ps.path), 0o770)
if err != nil {
return err
}
file, err = os.OpenFile(ps.path, os.O_RDWR|os.O_CREATE, 0o660)
}
if err != nil {
return err
}
defer file.Close()

ps.loadIfUpdated(ctx, file)
err = f(&ps.cached)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("update failed, rolling back")
ps.forceLoad(ctx, file)
return err
}

err = file.Truncate(0)
if err != nil {
return err
}

enc := json.NewEncoder(file)
enc.SetIndent("", " ")

err = enc.Encode(ps.cached)
if err != nil {
return err
}

err = file.Sync()
if err != nil {
return err
}

stat, err := file.Stat()
if err == nil {
ps.lastModTime = stat.ModTime()
}
return err
}
Loading

0 comments on commit ae4ccb3

Please sign in to comment.