github.com/hairyhenderson/gomplate/v4@v4.0.0-pre-2.0.20240520121557-362f058f0c93/internal/iohelpers/writers.go (about) 1 package iohelpers 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "io/fs" 10 "os" 11 "path/filepath" 12 "strings" 13 "sync" 14 15 "github.com/hack-pad/hackpadfs" 16 ) 17 18 type emptySkipper struct { 19 open func() (io.Writer, error) 20 21 // internal 22 w io.Writer 23 buf *bytes.Buffer 24 nw bool 25 } 26 27 // NewEmptySkipper creates an io.WriteCloser that will only start writing once a 28 // non-whitespace byte has been encountered. The wrapped io.WriteCloser must be 29 // provided by the `open` func. 30 func NewEmptySkipper(open func() (io.Writer, error)) io.WriteCloser { 31 return &emptySkipper{ 32 w: nil, 33 buf: &bytes.Buffer{}, 34 nw: false, 35 open: open, 36 } 37 } 38 39 func (f *emptySkipper) Write(p []byte) (n int, err error) { 40 if !f.nw { 41 if allWhitespace(p) { 42 // buffer the whitespace 43 return f.buf.Write(p) 44 } 45 46 // first time around, so open the writer 47 f.nw = true 48 f.w, err = f.open() 49 if err != nil { 50 return 0, err 51 } 52 if f.w == nil { 53 return 0, errors.New("nil writer returned by open") 54 } 55 // empty the buffer into the wrapped writer 56 _, err = f.buf.WriteTo(f.w) 57 if err != nil { 58 return 0, err 59 } 60 } 61 62 return f.w.Write(p) 63 } 64 65 // Close - implements io.Closer 66 func (f *emptySkipper) Close() error { 67 if wc, ok := f.w.(io.WriteCloser); ok { 68 return wc.Close() 69 } 70 return nil 71 } 72 73 func allWhitespace(p []byte) bool { 74 for _, b := range p { 75 if b == ' ' || b == '\t' || b == '\n' || b == '\r' || b == '\v' { 76 continue 77 } 78 return false 79 } 80 return true 81 } 82 83 // NopCloser returns a WriteCloser with a no-op Close method wrapping 84 // the provided io.Writer. 85 func NopCloser(w io.Writer) io.WriteCloser { 86 return &nopCloser{Writer: w} 87 } 88 89 type nopCloser struct { 90 io.Writer 91 } 92 93 // Close - implements io.Closer 94 func (n *nopCloser) Close() error { 95 return nil 96 } 97 98 var ( 99 _ io.WriteCloser = (*nopCloser)(nil) 100 _ io.WriteCloser = (*emptySkipper)(nil) 101 _ io.WriteCloser = (*sameSkipper)(nil) 102 ) 103 104 type sameSkipper struct { 105 open func() (io.WriteCloser, error) 106 107 // internal 108 r *bufio.Reader 109 w io.WriteCloser 110 buf *bytes.Buffer 111 diff bool 112 } 113 114 // SameSkipper creates an io.WriteCloser that will only start writing once a 115 // difference with the current output has been encountered. The wrapped 116 // io.WriteCloser must be provided by 'open'. 117 func SameSkipper(r io.Reader, open func() (io.WriteCloser, error)) io.WriteCloser { 118 br := bufio.NewReader(r) 119 return &sameSkipper{ 120 r: br, 121 w: nil, 122 buf: &bytes.Buffer{}, 123 diff: false, 124 open: open, 125 } 126 } 127 128 // Write - writes to the buffer, until a difference with the output is found, 129 // then flushes and writes to the wrapped writer. 130 func (f *sameSkipper) Write(p []byte) (n int, err error) { 131 if !f.diff { 132 in := make([]byte, len(p)) 133 _, err := f.r.Read(in) 134 if err != nil && err != io.EOF { 135 return 0, fmt.Errorf("failed to read: %w", err) 136 } 137 if bytes.Equal(in, p) { 138 return f.buf.Write(p) 139 } 140 141 f.diff = true 142 err = f.flush() 143 if err != nil { 144 return 0, err 145 } 146 } 147 return f.w.Write(p) 148 } 149 150 func (f *sameSkipper) flush() (err error) { 151 if f.w == nil { 152 f.w, err = f.open() 153 if err != nil { 154 return err 155 } 156 if f.w == nil { 157 return fmt.Errorf("nil writer returned by open") 158 } 159 } 160 // empty the buffer into the wrapped writer 161 _, err = f.buf.WriteTo(f.w) 162 return err 163 } 164 165 // Close - implements io.Closer 166 func (f *sameSkipper) Close() error { 167 // Check to see if we missed anything in the reader 168 if !f.diff { 169 n, err := f.r.Peek(1) 170 if len(n) > 0 || err != io.EOF { 171 err = f.flush() 172 if err != nil { 173 return fmt.Errorf("failed to flush on close: %w", err) 174 } 175 } 176 } 177 178 if f.w != nil { 179 return f.w.Close() 180 } 181 return nil 182 } 183 184 // LazyWriteCloser provides an interface to a WriteCloser that will open on the 185 // first access. The wrapped io.WriteCloser must be provided by 'open'. 186 func LazyWriteCloser(open func() (io.WriteCloser, error)) io.WriteCloser { 187 return &lazyWriteCloser{ 188 opened: sync.Once{}, 189 open: open, 190 } 191 } 192 193 type lazyWriteCloser struct { 194 w io.WriteCloser 195 // caches the error that came from open(), if any 196 openErr error 197 open func() (io.WriteCloser, error) 198 opened sync.Once 199 } 200 201 var _ io.WriteCloser = (*lazyWriteCloser)(nil) 202 203 func (l *lazyWriteCloser) openWriter() (r io.WriteCloser, err error) { 204 l.opened.Do(func() { 205 l.w, l.openErr = l.open() 206 }) 207 return l.w, l.openErr 208 } 209 210 func (l *lazyWriteCloser) Close() error { 211 w, err := l.openWriter() 212 if err != nil { 213 return err 214 } 215 return w.Close() 216 } 217 218 func (l *lazyWriteCloser) Write(p []byte) (n int, err error) { 219 w, err := l.openWriter() 220 if err != nil { 221 return 0, err 222 } 223 return w.Write(p) 224 } 225 226 // WriteFile writes the given content to the file, truncating any existing file, 227 // and creating the directory structure leading up to it if necessary. 228 func WriteFile(fsys fs.FS, filename string, content []byte) error { 229 err := assertPathInWD(filename) 230 if err != nil { 231 return fmt.Errorf("failed to open %s: %w", filename, err) 232 } 233 234 fi, err := fs.Stat(fsys, filename) 235 if err != nil && !errors.Is(err, fs.ErrNotExist) { 236 return fmt.Errorf("failed to stat %s: %w", filename, err) 237 } 238 mode := NormalizeFileMode(0o644) 239 if fi != nil { 240 mode = fi.Mode() 241 } 242 243 err = hackpadfs.MkdirAll(fsys, filepath.Dir(filename), 0o755) 244 if err != nil { 245 return fmt.Errorf("failed to make dirs for %s: %w", filename, err) 246 } 247 248 err = hackpadfs.WriteFullFile(fsys, filename, content, mode) 249 if err != nil { 250 return fmt.Errorf("failed to write %s: %w", filename, err) 251 } 252 253 return nil 254 } 255 256 func assertPathInWD(filename string) error { 257 wd, err := os.Getwd() 258 if err != nil { 259 return err 260 } 261 f, err := filepath.Abs(filename) 262 if err != nil { 263 return err 264 } 265 r, err := filepath.Rel(wd, f) 266 if err != nil { 267 return err 268 } 269 if strings.HasPrefix(r, "..") { 270 return fmt.Errorf("path %s not contained by working directory %s (rel: %s)", filename, wd, r) 271 } 272 return nil 273 }