Skip to content

Commit

Permalink
add filerotate package
Browse files Browse the repository at this point in the history
  • Loading branch information
kjk committed Oct 8, 2021
1 parent 4d4bdfb commit d68a108
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 2 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# common
code that I re-use among many Go projects
A bunch of Go packages that I use in multiple projects.
197 changes: 197 additions & 0 deletions filerotate/filerotate.go
Original file line number Diff line number Diff line change
@@ -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()
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/kjk/common

go 1.17

0 comments on commit d68a108

Please sign in to comment.