1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package lockedfile creates and manipulates files whose contents should only 6 // change atomically. 7 package lockedfile 8 9 import ( 10 "fmt" 11 "io" 12 "io/fs" 13 "os" 14 "runtime" 15 ) 16 17 // A File is a locked *os.File. 18 // 19 // Closing the file releases the lock. 20 // 21 // If the program exits while a file is locked, the operating system releases 22 // the lock but may not do so promptly: callers must ensure that all locked 23 // files are closed before exiting. 24 type File struct { 25 osFile 26 closed bool 27 } 28 29 // osFile embeds a *os.File while keeping the pointer itself unexported. 30 // (When we close a File, it must be the same file descriptor that we opened!) 31 type osFile struct { 32 *os.File 33 } 34 35 // OpenFile is like os.OpenFile, but returns a locked file. 36 // If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked; 37 // otherwise, it is read-locked. 38 func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) { 39 var ( 40 f = new(File) 41 err error 42 ) 43 f.osFile.File, err = openFile(name, flag, perm) 44 if err != nil { 45 return nil, err 46 } 47 48 // Although the operating system will drop locks for open files when the go 49 // command exits, we want to hold locks for as little time as possible, and we 50 // especially don't want to leave a file locked after we're done with it. Our 51 // Close method is what releases the locks, so use a finalizer to report 52 // missing Close calls on a best-effort basis. 53 runtime.SetFinalizer(f, func(f *File) { 54 panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", f.Name())) 55 }) 56 57 return f, nil 58 } 59 60 // Open is like os.Open, but returns a read-locked file. 61 func Open(name string) (*File, error) { 62 return OpenFile(name, os.O_RDONLY, 0) 63 } 64 65 // Create is like os.Create, but returns a write-locked file. 66 func Create(name string) (*File, error) { 67 return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 68 } 69 70 // Edit creates the named file with mode 0666 (before umask), 71 // but does not truncate existing contents. 72 // 73 // If Edit succeeds, methods on the returned File can be used for I/O. 74 // The associated file descriptor has mode O_RDWR and the file is write-locked. 75 func Edit(name string) (*File, error) { 76 return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666) 77 } 78 79 // Close unlocks and closes the underlying file. 80 // 81 // Close may be called multiple times; all calls after the first will return a 82 // non-nil error. 83 func (f *File) Close() error { 84 if f.closed { 85 return &fs.PathError{ 86 Op: "close", 87 Path: f.Name(), 88 Err: fs.ErrClosed, 89 } 90 } 91 f.closed = true 92 93 err := closeFile(f.osFile.File) 94 runtime.SetFinalizer(f, nil) 95 return err 96 } 97 98 // Read opens the named file with a read-lock and returns its contents. 99 func Read(name string) ([]byte, error) { 100 f, err := Open(name) 101 if err != nil { 102 return nil, err 103 } 104 defer f.Close() 105 106 return io.ReadAll(f) 107 } 108 109 // Write opens the named file (creating it with the given permissions if needed), 110 // then write-locks it and overwrites it with the given content. 111 func Write(name string, content io.Reader, perm fs.FileMode) (err error) { 112 f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 113 if err != nil { 114 return err 115 } 116 117 _, err = io.Copy(f, content) 118 if closeErr := f.Close(); err == nil { 119 err = closeErr 120 } 121 return err 122 } 123 124 // Transform invokes t with the result of reading the named file, with its lock 125 // still held. 126 // 127 // If t returns a nil error, Transform then writes the returned contents back to 128 // the file, making a best effort to preserve existing contents on error. 129 // 130 // t must not modify the slice passed to it. 131 func Transform(name string, t func([]byte) ([]byte, error)) (err error) { 132 f, err := Edit(name) 133 if err != nil { 134 return err 135 } 136 defer f.Close() 137 138 old, err := io.ReadAll(f) 139 if err != nil { 140 return err 141 } 142 143 new, err := t(old) 144 if err != nil { 145 return err 146 } 147 148 if len(new) > len(old) { 149 // The overall file size is increasing, so write the tail first: if we're 150 // about to run out of space on the disk, we would rather detect that 151 // failure before we have overwritten the original contents. 152 if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil { 153 // Make a best effort to remove the incomplete tail. 154 f.Truncate(int64(len(old))) 155 return err 156 } 157 } 158 159 // We're about to overwrite the old contents. In case of failure, make a best 160 // effort to roll back before we close the file. 161 defer func() { 162 if err != nil { 163 if _, err := f.WriteAt(old, 0); err == nil { 164 f.Truncate(int64(len(old))) 165 } 166 } 167 }() 168 169 if len(new) >= len(old) { 170 if _, err := f.WriteAt(new[:len(old)], 0); err != nil { 171 return err 172 } 173 } else { 174 if _, err := f.WriteAt(new, 0); err != nil { 175 return err 176 } 177 // The overall file size is decreasing, so shrink the file to its final size 178 // after writing. We do this after writing (instead of before) so that if 179 // the write fails, enough filesystem space will likely still be reserved 180 // to contain the previous contents. 181 if err := f.Truncate(int64(len(new))); err != nil { 182 return err 183 } 184 } 185 186 return nil 187 } 188