diff --git a/README.md b/README.md index cedf957..9b78389 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -# common -code that I re-use among many Go projects +A bunch of Go packages that I use in multiple projects. diff --git a/filerotate/filerotate.go b/filerotate/filerotate.go new file mode 100644 index 0000000..32ed73a --- /dev/null +++ b/filerotate/filerotate.go @@ -0,0 +1,197 @@ +package filerotate + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" +) + +type Config struct { + DidClose func(path string, didRotate bool) + PathIfShouldRotate func(creationTime time.Time, now time.Time) string +} + +type File struct { + sync.Mutex + + // Path is the path of the current file + Path string + + creationTime time.Time + + //Location *time.Location + + config Config + file *os.File + + // position in the file of last Write or Write2, exposed for tests + lastWritePos int64 +} + +func IsSameDay(t1, t2 time.Time) bool { + return t1.YearDay() == t2.YearDay() +} + +func IsSameHour(t1, t2 time.Time) bool { + return t1.YearDay() == t2.YearDay() && t1.Hour() == t2.Hour() +} + +func New(config *Config) (*File, error) { + if nil == config { + return nil, fmt.Errorf("must provide config") + } + if config.PathIfShouldRotate == nil { + return nil, fmt.Errorf("must provide config.ShouldRotate") + } + file := &File{ + config: *config, + } + err := file.reopenIfNeeded() + if err != nil { + return nil, err + } + return file, nil +} + +func NewDaily(dir string, didClose func(path string, didRotate bool)) (*File, error) { + daily := func(creationTime time.Time, now time.Time) string { + if IsSameDay(creationTime, now) { + return "" + } + name := now.Format("2006-01-02") + ".txt" + return filepath.Join(dir, name) + } + + config := Config{ + DidClose: didClose, + PathIfShouldRotate: daily, + } + return New(&config) +} + +func NewHourly(dir string, didClose func(path string, didRotate bool)) (*File, error) { + hourly := func(creationTime time.Time, now time.Time) string { + if IsSameHour(creationTime, now) { + return "" + } + name := now.Format("2006-01-02_15") + ".txt" + return filepath.Join(dir, name) + } + config := Config{ + DidClose: didClose, + PathIfShouldRotate: hourly, + } + return New(&config) +} + +func (f *File) close(didRotate bool) error { + if f.file == nil { + return nil + } + err := f.file.Close() + f.file = nil + if err == nil && f.config.DidClose != nil { + f.config.DidClose(f.Path, didRotate) + } + return err +} + +/* +func nowInMaybeLocation(loc *time.Location) time.Time { + now := time.Now() + if loc != nil { + now = now.In(loc) + } + return now +} +*/ + +func (f *File) open(path string) error { + f.Path = path + f.creationTime = time.Now() + // we can't assume that the dir for the file already exists + dir := filepath.Dir(f.Path) + err := os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + // would be easier to open with os.O_APPEND but Seek() doesn't work in that case + flag := os.O_CREATE | os.O_WRONLY + f.file, err = os.OpenFile(f.Path, flag, 0644) + if err != nil { + return err + } + _, err = f.file.Seek(0, io.SeekEnd) + return err +} + +func (f *File) reopenIfNeeded() error { + now := time.Now() + newPath := f.config.PathIfShouldRotate(f.creationTime, now) + if newPath == "" { + return nil + } + err := f.close(true) + if err != nil { + return err + } + return f.open(newPath) +} + +func (f *File) write(d []byte, flush bool) (int64, int, error) { + err := f.reopenIfNeeded() + if err != nil { + return 0, 0, err + } + f.lastWritePos, err = f.file.Seek(0, io.SeekCurrent) + if err != nil { + return 0, 0, err + } + n, err := f.file.Write(d) + if err != nil { + return 0, n, err + } + if flush { + err = f.file.Sync() + } + return f.lastWritePos, n, err +} + +// Write writes data to a file +func (f *File) Write(d []byte) (int, error) { + f.Lock() + defer f.Unlock() + + _, n, err := f.write(d, false) + return n, err +} + +// Write2 writes data to a file, optionally flushes. To enable users to later +// seek to where the data was written, it returns name of the file where data +// was written, offset at which the data was written, number of bytes and error +func (f *File) Write2(d []byte, flush bool) (string, int64, int, error) { + f.Lock() + defer f.Unlock() + + writtenAtPos, n, err := f.write(d, flush) + return f.Path, writtenAtPos, n, err +} + +func (f *File) Close() error { + f.Lock() + defer f.Unlock() + + return f.close(false) +} + +// Flush flushes the file +func (f *File) Flush() error { + f.Lock() + defer f.Unlock() + + return f.file.Sync() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..56592b7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/kjk/common + +go 1.17