// Package main is the command-line entrypoint for the distribusi command. package main import ( "bytes" "encoding/base64" "fmt" "image/png" "io/fs" "io/ioutil" "net/http" "os" "os/exec" "path" "path/filepath" "sort" "strings" logrusStack "github.com/Gurpartap/logrus-stack" "github.com/barasher/go-exiftool" "github.com/disintegration/imaging" "github.com/gabriel-vasile/mimetype" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) // exiftooInstalled tells us if the exiftool binary is installed or not. var exiftoolInstalled = true // generatedInDistribusi is an internal marker to help recognise when // distribusi-go has generated files. var generatedInDistribusi = "" // htmlBody is the template for the index.html files that are generated by distribusi-go. var htmlBody = `
%s %s ../" div, err := mkDiv(c, "os/directory", href, "menu", false) if err != nil { return err } html = append(html, div) } for _, dir := range dirs { fname := filepath.Base(dir) if skip := shouldSkip(c, dir, ignore); skip { continue } mtype := "os/directory" href := fmt.Sprintf("%s/", fname, fname) div, err := mkDiv(c, mtype, href, fname, false) if err != nil { return err } html = append(html, div) } for _, file := range files { fname := filepath.Base(file) if skip := shouldSkip(c, file, ignore); skip { continue } mtype, err := getMtype(file) if err != nil { return err } unknown, href, err := getHref(c, file, mtype) if err != nil { return err } div, err := mkDiv(c, mtype, href, fname, unknown) if err != nil { return err } html = append(html, div) } if err := writeIndex(absPath, html, c.String("css")); err != nil { return err } allDirs = append(allDirs, strings.Join(dirs, " ")) } return nil }); err != nil { return err } logrus.Debugf("generated files in following paths: %s", strings.Join(allDirs, " ")) return nil } // 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 } // getMtype determines file mimetype which helps to arrange HTML tags // appropriate for the file type. func getMtype(fpath string) (string, error) { mtype, err := mimetype.DetectFile(fpath) if err != nil { return "", err } return mtype.String(), nil } // genThumb generates an in-memory image thumbnail and encodes it into a base64 // representation which is suitable for embedding in HTML pages. func genThumb(c *cli.Context, fpath, caption string) (string, error) { knownFailureExts := []string{".ico", ".svg", ".xcf"} if sliceContains(knownFailureExts, filepath.Ext(fpath)) { return "", nil } imgSrc, err := imaging.Open(fpath, imaging.AutoOrientation(true)) if err != nil { logrus.Debugf("failed to generate thumbnail for %s", fpath) return "", err } img := imaging.Thumbnail(imgSrc, 450, 450, imaging.Lanczos) buf := new(bytes.Buffer) png.Encode(buf, img) imgBase64Str := base64.StdEncoding.EncodeToString(buf.Bytes()) return imgBase64Str, nil } // getCaption retrieves an embedded image caption via exif-tool. If not // exiftool is installed, we gracefully bail out. The caller is responsible for // handling the alternative. func getCaption(c *cli.Context, fpath string) (string, error) { var caption string if !exiftoolInstalled { return "", nil } exif, err := exiftool.NewExiftool() if err != nil { return caption, fmt.Errorf("failed to initialise exiftool, saw %v", err) } defer exif.Close() for _, finfo := range exif.ExtractMetadata(fpath) { if finfo.Err != nil { continue } for k, v := range finfo.Fields { if k == "Comment" { caption = fmt.Sprintf("%v", v) } } } return caption, nil } // parseMtype parses a mimetype string to simplify programmatic type lookups. func parseMtype(mtype string) (string, string) { stripCharset := strings.Split(mtype, ";") splitTypes := strings.Split(stripCharset[0], "/") ftype, stype := splitTypes[0], splitTypes[1] return ftype, stype } // sliceContains checks if an element is present in a list. func sliceContains(items []string, target string) bool { for _, item := range items { if item == target { return true } } return false } // trimFinalNewline trims newlines from the end of bytes just read from files. func trimFinalNewline(contents []byte) string { return strings.TrimSuffix(string(contents), "\n") } // getHref figures out which href tag corresponds to which file by navigating // the mimetype. If a type of file is unknown, this is signalled via the bool // return value. func getHref(c *cli.Context, fpath string, mtype string) (bool, string, error) { var href string var caption string var unknown bool fname := filepath.Base(fpath) ftype, stype := parseMtype(mtype) if ftype == "text" { fcontents, err := os.ReadFile(fpath) if err != nil { return unknown, href, err } if stype == "html" { href = fmt.Sprintf("%s", trimFinalNewline(fcontents)) } } else if ftype == "image" { caption = "" exifCaption, err := getCaption(c, fpath) if err != nil { return unknown, href, nil } if exifCaption != "" { caption = exifCaption } if stype == "gif" { href = fmt.Sprintf("", fname) } else { thumb, err := genThumb(c, fpath, caption) if err != nil { unknown = true href = fmt.Sprintf("%s", stype, fname, fname) } else { href = fmt.Sprintf( "", fname, thumb, caption, ) } } } else if ftype == "application" { if stype == "pdf" { href = fmt.Sprintf("", fname, fname) } else { unknown = true href = fmt.Sprintf("%s", stype, fname, fname) } } else if ftype == "audio" { href = fmt.Sprintf("", fname, stype) } else if ftype == "video" { href = fmt.Sprintf("", fname, stype) } else { unknown = true href = fmt.Sprintf("%s", stype, fname, fname) } return unknown, href, nil } // mkDiv cosntructs a HTML div for inclusion in the generated index.html. func mkDiv(c *cli.Context, mtype string, href, fname string, unknown bool) (string, error) { var div string filename := fmt.Sprintf("%s", fname) ftype, _ := parseMtype(mtype) if ftype == "text" { div = fmt.Sprintf("