// Package main provides the command-line entrypoint.
package main
import (
"errors"
"flag"
"fmt"
"io/fs"
"io/ioutil"
"log/slog"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
thumb "github.com/prplecake/go-thumbnail"
)
// logger is the global logger.
var logger *slog.Logger
// thumbnailSuffix is the file name suffix used for generated thumbnails.
var thumbnailSuffix = "_distribusi_thumbnail"
// logStripMsg strips content from logs messages for brevity.
var logStripMsg = "...stripped from logs for brevity..."
// port is for serving locally
var port = ":1312"
// htmlBody is the template for the index.html files that are generated by distribusi-go.
var htmlBody = `
%s
%s
%s`,
"generic": `
%s
`,
},
"image": {
"thumbnail": trimAllNewlines(``),
"generic": trimAllNewlines(``),
},
"application": {
"pdf": trimAllNewlines(``),
},
"audio": {
"generic": trimAllNewlines(``),
},
"video": {
"generic": trimAllNewlines(``),
},
"unknown": {
"generic": `%s`,
},
}
// generatedInDistribusi is an internal marker to help recognise when
// distribusi-go has generated files.
var generatedInDistribusi = ""
var helpOutput = `USAGE:
distribusi [options] [arguments]
DESCRIPTION:
A low-tech content management system for the web that produces static index
pages based on folders in the files system. It is inspired by the automatic
index functions featured in several popular web servers.
ARGUMENTS:
path to distribusify (default: ".")
OPTIONS:
-d show debug output
-c css file for custom styles
-h output help
-i ignore paths (e.g. "*.gif")
-s serve locally
-v output version
-w wipe generated files
`
var (
cssFlag string
ignoreFlag string
debugFlag bool
helpFlag bool
serveFlag bool
wipeFlag bool
version = "0.2.0"
versionFlag bool
)
func main() {
flag.StringVar(&cssFlag, "c", "", "")
flag.StringVar(&ignoreFlag, "i", "", "")
flag.BoolVar(&debugFlag, "d", false, "")
flag.BoolVar(&helpFlag, "h", false, "")
flag.BoolVar(&serveFlag, "s", false, "")
flag.BoolVar(&versionFlag, "v", false, "")
flag.BoolVar(&wipeFlag, "w", false, "")
flag.Usage = func() {
return
}
flag.Parse()
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == "time" {
t := a.Value.Time()
return slog.String("time", t.Format(time.Kitchen))
}
return a
},
}
if debugFlag {
opts.Level = slog.LevelDebug
opts.AddSource = true
}
logger = slog.New(slog.NewTextHandler(os.Stdout, opts))
slog.SetDefault(logger)
if helpFlag {
fmt.Print(helpOutput)
os.Exit(0)
}
if versionFlag {
fmt.Printf("%s\n", version)
os.Exit(0)
}
path, err := os.Getwd()
if err != nil {
logger.Error(fmt.Sprintf("unable to determine current working directory: %s", err))
}
if len(flag.Args()) == 1 {
path = flag.Args()[0]
}
root, err := filepath.Abs(path)
if err != nil {
logger.Error(fmt.Sprintf("unable to determine absolute path of %s: %s", path, err))
}
if _, err := os.Stat(root); os.IsNotExist(err) {
logger.Error(fmt.Sprintf("%s does not exist?", root))
}
logger.Debug(fmt.Sprintf("selecting %s as distribusi root", root))
var ignore []string
if ignoreFlag != "" {
ignore = strings.Split(ignoreFlag, ",")
for i := range ignore {
ignore[i] = strings.TrimSpace(ignore[i])
}
logger.Debug(fmt.Sprintf("parsed %s as ignore patterns", strings.Join(ignore, " ")))
}
if wipeFlag {
if err := wipeGeneratedFiles(root); err != nil {
logger.Error(err.Error())
}
logger.Info(fmt.Sprintf("wiped generated files in %s", root))
return
}
ch := make(chan error, 2)
go func() {
if err := distribusify(root, ignore); err != nil {
ch <- err
return
}
msg := " done!"
if serveFlag {
fmt.Printf(msg)
} else {
fmt.Println(msg)
}
ch <- nil
return
}()
if serveFlag {
go func() {
logger.Debug("attempting to start up the web server")
if err := serveHTTP(root); err != nil {
ch <- err
return
}
ch <- nil
return
}()
} else {
// NOTE(d1): close the channel, we're not serving anything
ch <- nil
}
for i := 1; i <= 2; i++ {
err := <-ch
if err != nil {
logger.Error(err.Error())
}
}
}
// removeIndex safely removes an index.html, making sure to check that
// distribusi generated it.
func removeIndex(fpath string) error {
file, err := os.ReadFile(fpath)
if err != nil {
return err
}
if strings.Contains(string(file), generatedInDistribusi) {
if err := os.Remove(fpath); err != nil {
return err
}
return nil
}
return nil
}
// wipeGeneratedFiles removes all distribusi generated files under a file
// system path. We do take care to avoid deleting files that distribusi has not
// generated by checking their contents.
func wipeGeneratedFiles(dir string) error {
if err := filepath.WalkDir(dir, func(fpath string, dirEntry fs.DirEntry, err error) error {
fname := filepath.Base(fpath)
if fname == "index.html" {
if err := removeIndex(fpath); err != nil {
return fmt.Errorf("unable to remove %s: %s", fpath, err)
}
logger.Debug(fmt.Sprintf("wiping %s as requested", fpath))
} else if strings.Contains(fname, thumbnailSuffix) {
if err := os.Remove(fpath); err != nil {
return fmt.Errorf("unable to remove %s: %s", fpath, err)
}
logger.Debug(fmt.Sprintf("wiping %s as requested", fpath))
}
return nil
}); err != nil {
return err
}
return nil
}
// createDiv cosntructs a HTML div for inclusion in the generated index.html. These
// dives are used to wrap the elements that appear on generated pages with
// relevant identifiers for convenient styling.
func createDiv(mtype string, href, fname string, unknown bool) (string, error) {
var div string
var divTemplate string
var strippedDebugOutput string
filename := fmt.Sprintf("%s", fname)
ftype, stype, err := parseMtype(mtype)
if err != nil {
return div, err
}
if ftype == "text" {
divTemplate = "
%s%s
"
div = fmt.Sprintf(divTemplate, fname, ftype, href, filename)
strippedDebugOutput = fmt.Sprintf(divTemplate, fname, ftype, logStripMsg, filename)
} else if ftype == "os" {
if stype == "directory" {
divTemplate = "
%s
"
div = fmt.Sprintf(divTemplate, fname, stype, href)
} else {
// don't include filename since link already has it
divTemplate = "
%s
"
div = fmt.Sprintf(divTemplate, fname, ftype, href)
}
} else {
if unknown {
// don't include filename since link already has it
divTemplate = "