package files import ( "compress/gzip" "io" "os" "path/filepath" "go.alanpearce.eu/homestead/internal/buffer" "go.alanpearce.eu/homestead/internal/content" "go.alanpearce.eu/homestead/internal/multifile" "go.alanpearce.eu/homestead/internal/storage" "go.alanpearce.eu/x/log" "github.com/andybalholm/brotli" "github.com/klauspost/compress/zstd" "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, errors.E) { return &Files{ outputDirectory: outputDirectory, options: opts, log: logger, }, nil } func (f *Files) OpenRead(filename string) (io.ReadCloser, errors.E) { file, err := os.Open(f.join(filename)) if err != nil { return nil, errors.WithStack(err) } return file, nil } func (f *Files) OpenWrite(filename string) (io.WriteCloser, errors.E) { return openFileWrite(f.join(filename)) } func (f *Files) WritePost(post *content.Post, content *buffer.Buffer) errors.E { fd, err := f.write(post.URL, content) if err != nil { return err } if err := fd.Close(); err != nil { return errors.WithStack(err) } if mf, isMultifile := fd.(*multifile.MultiFile); isMultifile { err = mf.Chtimes(post.Date) } else { err = errors.WithStack(os.Chtimes(fd.Name(), post.Date, post.Date)) } if err != nil { return errors.WithMessage(err, "could not set file times") } return nil } func (f *Files) Write(pathname string, _ string, content *buffer.Buffer) errors.E { fd, err := f.write(pathname, content) if err != nil { return err } fd.Close() return nil } func (f *Files) NewFileFromPost(post *content.Post) *storage.File { return &storage.File{ Path: post.URL, Encodings: map[string]*buffer.Buffer{}, } } func (f *Files) WriteFile(file *storage.File, content *buffer.Buffer) errors.E { return f.Write(file.Path, file.Title, content) } func (f *Files) write(pathname string, content *buffer.Buffer) (multifile.FileLike, errors.E) { filename := pathNameToFileName(pathname) err := f.Mkdirp(filepath.Dir(filename)) if err != nil { return nil, errors.WithMessage(err, "could not create directory") } fd, err := f.OpenFileAndVariants(filename) if err != nil { return nil, errors.WithMessagef(err, "could not open output file") } if _, err := fd.Write(content.Bytes()); err != nil { return nil, errors.WithMessage(err, "could not write output file") } return fd, nil } func openFileWrite(filename string) (*os.File, errors.E) { f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return nil, errors.WithMessage(err, "could not open output file") } return f, nil } func openFileGz(filename string) (*multifile.CompressWriter, errors.E) { filenameGz := filename + ".gz" f, err := openFileWrite(filenameGz) if err != nil { return nil, err } var w io.WriteCloser var baseErr error w, baseErr = gzip.NewWriterLevel(f, gzipLevel) if baseErr != nil { return nil, errors.WithStack(baseErr) } return multifile.NewCompressWriter(f, w), err } func openFileBrotli(filename string) (*multifile.CompressWriter, errors.E) { filenameBrotli := filename + ".br" f, err := openFileWrite(filenameBrotli) if err != nil { return nil, err } return multifile.NewCompressWriter(f, brotli.NewWriterLevel(f, brotliLevel)), nil } func openFileZstd(filename string) (*multifile.CompressWriter, errors.E) { f, err := openFileWrite(filename + ".zstd") if err != nil { return nil, err } var w io.WriteCloser var baseErr error w, baseErr = zstd.NewWriter(f) if baseErr != nil { return nil, errors.WithStack(baseErr) } return multifile.NewCompressWriter(f, w), nil } func multiOpenFile(filename string) (*multifile.MultiFile, errors.E) { 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 } zst, err := openFileZstd(filename) if err != nil { return nil, err } return multifile.NewMultiFile(r, gz, br, zst), nil } func (f *Files) OpenFileAndVariants(filename string) (multifile.FileLike, errors.E) { if f.options.Compress { return multiOpenFile(f.join(filename)) } return openFileWrite(f.join(filename)) } func (f *Files) Mkdirp(dir string) errors.E { err := os.MkdirAll(f.join(dir), 0755) if err != nil { return errors.WithMessage(err, "could not create directory") } return nil } func (f *Files) join(filename string) string { return filepath.Join(f.outputDirectory, pathNameToFileName(filename)) }