package files
import (
"compress/gzip"
"io"
"os"
"path/filepath"
"alin.ovh/homestead/domain/content"
"alin.ovh/homestead/shared/buffer"
"alin.ovh/homestead/shared/file"
"alin.ovh/homestead/shared/storage"
"alin.ovh/x/log"
"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fmsg"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/zstd"
)
type Files struct {
outputDirectory string
options *Options
log *log.Logger
}
type Options struct {
Compress bool
}
const (
gzipLevel = 6
brotliLevel = 9
)
func NewWriter(outputDirectory string, logger *log.Logger, opts *Options) (*Files, error) {
return &Files{
outputDirectory: outputDirectory,
options: opts,
log: logger,
}, nil
}
func (f *Files) OpenRead(filename string) (io.ReadCloser, error) {
file, err := os.Open(f.join(filename))
if err != nil {
return nil, fault.Wrap(err)
}
return file, nil
}
func (f *Files) OpenWrite(filename string) (io.WriteCloser, error) {
return openFileWrite(f.join(filename))
}
func (f *Files) WritePost(post *content.Post, content *buffer.Buffer) error {
fd, err := f.write(post.URL, content)
if err != nil {
return err
}
if err := fd.Close(); err != nil {
return fault.Wrap(err)
}
if mf, isMultifile := fd.(*file.MultiFile); isMultifile {
err = mf.Chtimes(post.Date)
} else {
err = fault.Wrap(os.Chtimes(fd.Name(), post.Date, post.Date))
}
if err != nil {
return fault.Wrap(err, fmsg.With("could not set file times"))
}
return nil
}
func (f *Files) Write(pathname string, _ string, content *buffer.Buffer) error {
fd, err := f.write(pathname, content)
if err != nil {
return err
}
if err := fd.Close(); err != nil {
return fault.Wrap(err)
}
return nil
}
func (f *Files) NewFileFromPost(post *content.Post) *storage.File {
return &storage.File{
Path: post.URL,
FSPath: pathNameToFileName(post.URL),
Encodings: map[string]*buffer.Buffer{},
}
}
func (f *Files) WriteFile(file *storage.File, content *buffer.Buffer) error {
return f.Write(file.Path, file.Title, content)
}
func (f *Files) OpenFileAndVariants(filename string) (file.FileLike, error) {
if f.options.Compress {
return multiOpenFile(f.join(filename))
}
return openFileWrite(f.join(filename))
}
func (f *Files) Mkdirp(dir string) error {
err := os.MkdirAll(f.join(dir), 0o750)
if err != nil {
return fault.Wrap(err, fmsg.With("could not create directory"))
}
return nil
}
func (f *Files) write(pathname string, content *buffer.Buffer) (file.FileLike, error) {
filename := pathNameToFileName(pathname)
err := f.Mkdirp(filepath.Dir(filename))
if err != nil {
return nil, fault.Wrap(err, fmsg.With("could not create directory"))
}
fd, err := f.OpenFileAndVariants(filename)
if err != nil {
return nil, fault.Wrap(err, fmsg.With("could not open output file"))
}
if _, err := fd.Write(content.Bytes()); err != nil {
return nil, fault.Wrap(err, fmsg.With("could not write output file"))
}
return fd, nil
}
func openFileWrite(filename string) (*os.File, error) {
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return nil, fault.Wrap(err, fmsg.With("could not open output file"))
}
return f, nil
}
func openFileGz(filename string) (*file.CompressWriter, error) {
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, fault.Wrap(baseErr)
}
return file.NewCompressWriter(f, w), err
}
func openFileBrotli(filename string) (*file.CompressWriter, error) {
filenameBrotli := filename + ".br"
f, err := openFileWrite(filenameBrotli)
if err != nil {
return nil, err
}
return file.NewCompressWriter(f, brotli.NewWriterLevel(f, brotliLevel)), nil
}
func openFileZstd(filename string) (*file.CompressWriter, error) {
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, fault.Wrap(baseErr)
}
return file.NewCompressWriter(f, w), nil
}
func multiOpenFile(filename string) (*file.MultiFile, 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
}
zst, err := openFileZstd(filename)
if err != nil {
return nil, err
}
return file.NewMultiFile(r, gz, br, zst), nil
}
func (f *Files) join(filename string) string {
return filepath.Join(f.outputDirectory, pathNameToFileName(filename))
}
shared/storage/files/writer.go (view raw)