diff --git a/chown.go b/chown.go new file mode 100644 index 0000000..3b3f9bc --- /dev/null +++ b/chown.go @@ -0,0 +1,12 @@ +// +build !linux + +package lumberjack + +import ( + "os" + "syscall" +) + +func chown(_ string, _ os.FileInfo) error { + return nil +} diff --git a/chown_linux.go b/chown_linux.go new file mode 100644 index 0000000..2758ec9 --- /dev/null +++ b/chown_linux.go @@ -0,0 +1,19 @@ +package lumberjack + +import ( + "os" + "syscall" +) + +// os_Chown is a var so we can mock it out during tests. +var os_Chown = os.Chown + +func chown(name string, info os.FileInfo) error { + f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + f.Close() + stat := info.Sys().(*syscall.Stat_t) + return os_Chown(name, int(stat.Uid), int(stat.Gid)) +} diff --git a/chown_test.go b/chown_test.go new file mode 100644 index 0000000..1bfb245 --- /dev/null +++ b/chown_test.go @@ -0,0 +1,67 @@ +// +build linux + +package lumberjack + +import ( + "os" + "syscall" + "testing" +) + +func TestMaintainOwner(t *testing.T) { + fakeC := fakeChown{} + os_Chown = fakeC.Set + os_Stat = fakeStat + defer func() { + os_Chown = os.Chown + os_Stat = os.Stat + }() + currentTime = fakeTime + dir := makeTempDir("TestMaintainOwner", t) + defer os.RemoveAll(dir) + + filename := logFile(dir) + + l := &Logger{ + Filename: filename, + MaxBackups: 1, + MaxSize: 100, // megabytes + } + defer l.Close() + b := []byte("boo!") + n, err := l.Write(b) + isNil(err, t) + equals(len(b), n, t) + + newFakeTime() + + err = l.Rotate() + isNil(err, t) + + equals(555, fakeC.uid, t) + equals(666, fakeC.gid, t) +} + +type fakeChown struct { + name string + uid int + gid int +} + +func (f *fakeChown) Set(name string, uid, gid int) error { + f.name = name + f.uid = uid + f.gid = gid + return nil +} + +func fakeStat(name string) (os.FileInfo, error) { + info, err := os.Stat(name) + if err != nil { + return info, err + } + stat := info.Sys().(*syscall.Stat_t) + stat.Uid = 555 + stat.Gid = 666 + return info, nil +} diff --git a/lumberjack.go b/lumberjack.go index c05e646..2e5d283 100644 --- a/lumberjack.go +++ b/lumberjack.go @@ -101,6 +101,9 @@ var ( // currentTime exists so it can be mocked out by tests. currentTime = time.Now + // os_Stat exists so it can be mocked out by tests. + os_Stat = os.Stat + // megabyte is the conversion factor between MaxSize and bytes. It is a // variable so tests can mock it out and not need to write megabytes of data // to disk. @@ -191,19 +194,27 @@ func (l *Logger) openNew() error { } name := l.filename() - _, err = os.Stat(name) + mode := os.FileMode(0644) + info, err := os_Stat(name) if err == nil { + // Copy the mode off the old logfile. + mode = info.Mode() // move the existing file newname := backupName(name, l.LocalTime) if err := os.Rename(name, newname); err != nil { return fmt.Errorf("can't rename log file: %s", err) } + + // this is a no-op anywhere but linux + if err := chown(name, info); err != nil { + return err + } } // we use truncate here because this should only get called when we've moved // the file ourselves. if someone else creates the file in the meantime, // just wipe out the contents. - f, err := os.OpenFile(l.filename(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) if err != nil { return fmt.Errorf("can't open new logfile: %s", err) } @@ -234,7 +245,7 @@ func backupName(name string, local bool) string { // put it over the MaxSize, a new file is created. func (l *Logger) openExistingOrNew(writeLen int) error { filename := l.filename() - info, err := os.Stat(filename) + info, err := os_Stat(filename) if os.IsNotExist(err) { return l.openNew() } diff --git a/lumberjack_test.go b/lumberjack_test.go index d31889b..29ca7a0 100644 --- a/lumberjack_test.go +++ b/lumberjack_test.go @@ -493,7 +493,6 @@ func TestRotate(t *testing.T) { existsWithLen(filename2, n, t) existsWithLen(filename, 0, t) fileCount(dir, 2, t) - newFakeTime() err = l.Rotate() @@ -574,6 +573,43 @@ localtime = true`[1:] equals(0, len(md.Undecoded()), t) } +func TestMaintainMode(t *testing.T) { + currentTime = fakeTime + dir := makeTempDir("TestMaintainMode", t) + defer os.RemoveAll(dir) + + filename := logFile(dir) + + mode := os.FileMode(0770) + f, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, mode) + isNil(err, t) + f.Close() + + l := &Logger{ + Filename: filename, + MaxBackups: 1, + MaxSize: 100, // megabytes + } + defer l.Close() + b := []byte("boo!") + n, err := l.Write(b) + isNil(err, t) + equals(len(b), n, t) + + newFakeTime() + + err = l.Rotate() + isNil(err, t) + + filename2 := backupFile(dir) + info, err := os.Stat(filename) + isNil(err, t) + info2, err := os.Stat(filename2) + isNil(err, t) + equals(mode, info.Mode(), t) + equals(mode, info2.Mode(), t) +} + // makeTempDir creates a file with a semi-unique name in the OS temp directory. // It should be based on the name of the test, to keep parallel tests from // colliding, and must be cleaned up after the test is finished.