diff --git a/blogo/blogo.go b/blogo/blogo.go new file mode 100644 index 0000000..0d37dd7 --- /dev/null +++ b/blogo/blogo.go @@ -0,0 +1,121 @@ +package blogo + +import ( + "errors" + "io" + "io/fs" + "log/slog" + "net/http" +) + +type Options struct { + Logger *slog.Logger +} + +type Blogo struct { + files fs.FS + sources []SourcerPlugin + + log *slog.Logger + panic bool +} + +func New(opts ...Options) *Blogo { + opt := Options{} + if len(opts) > 0 { + opt = opts[0] + } + + if opt.Logger == nil { + opt.Logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + } else { + opt.Logger = opt.Logger.WithGroup("blogo") + } + + return &Blogo{ + files: nil, + sources: []SourcerPlugin{}, + log: opt.Logger, + panic: true, // TODO + } +} + +func (b *Blogo) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if b.files == nil { + err := b.Init() + if err != nil { + err = errors.Join(errors.New("failed to initialize Blogo engine on first request"), err) + if b.panic { + panic(err.Error()) + } else { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + } + return + } + } + + f, err := b.files.Open(r.URL.Path) + if errors.Is(err, fs.ErrNotExist) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(err.Error())) + return + } else if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + defer f.Close() + + buf := make([]byte, 1024) + for { + n, err := f.Read(buf) + if err != nil && err != io.EOF { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + } + + if n == 0 { + break + } + + _, err = w.Write(buf[:n]) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + } + } +} + +func (b *Blogo) Use(p Plugin) { + if p, ok := p.(SourcerPlugin); ok { + b.log.Debug("Added sourcer plugin", slog.String("plugin", p.Name())) + b.sources = append(b.sources, p) + } +} + +func (b *Blogo) Init() error { + b.log.Debug("Initializing blogo") + + fs, err := b.sourceFiles() + if err != nil { + return errors.Join(errors.New("failed to source files"), err) + } + b.files = fs + + return nil +} + +type emptyFS struct{} + +func (f emptyFS) Open(name string) (fs.File, error) { + return nil, fs.ErrNotExist +} + +func (b *Blogo) sourceFiles() (fs.FS, error) { + if len(b.sources) == 0 { + return emptyFS{}, nil + } + + return b.sources[0].Source() // TODO: Support for multiple sources (with or without preffixes, first-read, etc etc) +} diff --git a/blogo/plugins.go b/blogo/plugins.go new file mode 100644 index 0000000..fb7a08e --- /dev/null +++ b/blogo/plugins.go @@ -0,0 +1,12 @@ +package blogo + +import "io/fs" + +type Plugin interface { + Name() string +} + +type SourcerPlugin interface { + Plugin + Source() (fs.FS, error) +}