diff options
author | Alan Pearce | 2025-01-26 11:52:12 +0100 |
---|---|---|
committer | Alan Pearce | 2025-01-26 13:38:16 +0100 |
commit | 7d9e98a0996eca0372e38cee4b8826d84a9ace2b (patch) | |
tree | 6ae88100a923012f04ec316a45990b6902140136 /internal/storage | |
parent | 93c01942cb379b448dafab7ceffd78c005772928 (diff) | |
download | website-7d9e98a0996eca0372e38cee4b8826d84a9ace2b.tar.lz website-7d9e98a0996eca0372e38cee4b8826d84a9ace2b.tar.zst website-7d9e98a0996eca0372e38cee4b8826d84a9ace2b.zip |
refactor outputs->storage for generalisation
Diffstat (limited to 'internal/storage')
-rw-r--r-- | internal/storage/files/writer.go | 212 |
1 files changed, 212 insertions, 0 deletions
diff --git a/internal/storage/files/writer.go b/internal/storage/files/writer.go new file mode 100644 index 0000000..6f10fe7 --- /dev/null +++ b/internal/storage/files/writer.go @@ -0,0 +1,212 @@ +package files + +import ( + "compress/gzip" + "context" + "io" + "io/fs" + "os" + "path" + "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 := path.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 := path.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 := path.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(path.Join(f.outputDirectory, dir), 0755) + + return errors.WithMessage(err, "could not create directory") +} |