package files import ( "compress/gzip" "context" "io" "io/fs" "os" "path/filepath" "go.alanpearce.eu/x/log" "github.com/a-h/templ" "github.com/andybalholm/brotli" "gitlab.com/tozd/go/errors" ) const ( gzipLevel = 6 brotliLevel = 9 ) type Files struct { outputDirectory string options *Options log *log.Logger } type Options struct { Compress bool } func NewWriter(outputDirectory string, logger *log.Logger, opts *Options) *Files { return &Files{ outputDirectory: outputDirectory, options: opts, log: logger, } } func (f *Files) CopyRecursive(src string) error { return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, path) if err != nil { return err } if d.IsDir() { return f.mkdirp(rel) } sf, err := os.Open(path) if err != nil { return err } defer sf.Close() df, err := f.openFileAndVariants(filepath.Join(f.outputDirectory, rel)) if err != nil { return err } defer df.Close() if _, err := io.Copy(df, sf); err != nil { return err } return nil }) } func (f *Files) Open(filename string) (io.ReadCloser, error) { return os.Open(filepath.Join(f.outputDirectory, filename)) } func (f *Files) OutputToFile(output io.Reader, filename string) error { fn := filepath.Join(f.outputDirectory, filename) if err := f.mkdirp(filepath.Dir(filename)); err != nil { return err } f.log.Debug("outputting file", "filename", fn) file, err := f.openFileAndVariants(fn) if err != nil { return errors.WithMessage(err, "could not open output file") } defer file.Close() if _, err := io.Copy(file, output); err != nil { return errors.WithMessage(err, "could not write output file") } return nil } func (f *Files) RenderToFile(component templ.Component, filename string) error { fn := filepath.Join(f.outputDirectory, filename) if err := f.mkdirp(filepath.Dir(filename)); err != nil { return err } f.log.Debug("outputting file", "filename", filename) file, err := f.openFileAndVariants(fn) if err != nil { return errors.WithMessage(err, "could not open output file") } defer file.Close() if err := component.Render(context.TODO(), file); err != nil { return errors.WithMessage(err, "could not write output file") } return nil } func (f *Files) WriterToFile(writer io.WriterTo, filename string) error { fn := filepath.Join(f.outputDirectory, filename) if err := f.mkdirp(filepath.Dir(filename)); err != nil { return err } f.log.Debug("outputting file", "filename", fn) file, err := f.openFileAndVariants(fn) if err != nil { return errors.WithMessage(err, "could not open output file") } defer file.Close() if _, err := writer.WriteTo(file); err != nil { return errors.WithMessage(err, "could not write output file") } return nil } type MultiWriteCloser struct { writers []io.WriteCloser multiWriter io.Writer } func (mw *MultiWriteCloser) Write(p []byte) (n int, err error) { return mw.multiWriter.Write(p) } func (mw *MultiWriteCloser) Close() error { var lastErr error for _, w := range mw.writers { err := w.Close() if err != nil { lastErr = err } } return lastErr } func openFileWrite(filename string) (*os.File, error) { return os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) } func openFileGz(filename string) (*gzip.Writer, error) { filenameGz := filename + ".gz" f, err := openFileWrite(filenameGz) if err != nil { return nil, err } return gzip.NewWriterLevel(f, gzipLevel) } func openFileBrotli(filename string) (*brotli.Writer, error) { filenameBrotli := filename + ".br" f, err := openFileWrite(filenameBrotli) if err != nil { return nil, err } return brotli.NewWriterLevel(f, brotliLevel), nil } func multiOpenFile(filename string) (*MultiWriteCloser, error) { r, err := openFileWrite(filename) if err != nil { return nil, err } gz, err := openFileGz(filename) if err != nil { return nil, err } br, err := openFileBrotli(filename) if err != nil { return nil, err } return &MultiWriteCloser{ writers: []io.WriteCloser{r, gz, br}, multiWriter: io.MultiWriter(r, gz, br), }, nil } func (f *Files) openFileAndVariants(filename string) (io.WriteCloser, error) { if f.options.Compress { return multiOpenFile(filename) } return openFileWrite(filename) } func (f *Files) mkdirp(dir string) error { f.log.Debug("creating directory", "dir", dir) err := os.MkdirAll(filepath.Join(f.outputDirectory, dir), 0755) return errors.WithMessage(err, "could not create directory") }