All ProjectsHome
csb
csb/src/csb-tree.go
csb-tree.go Raw
package main

import (
	"errors"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"

	"github.com/docopt/docopt-go"
	"github.com/dustin/go-humanize"
)

type pageContext struct {
	Project  string
	Name     string
	Content  content
	Children []child
	Path     pathwrapper
}

type pathwrapper string

func (p pathwrapper) Segments() []pathsegment {
	sections := strings.Split(string(p), "/")
	r := make([]pathsegment, len(sections))
	for i, s := range sections {
		r[i] = pathsegment{Name: s, Path: strings.Join(sections[:i+1], "/")}
	}
	return r
}

type pathsegment struct {
	Name string
	Path string
}

type content struct {
	Name     string
	Rendered template.HTML
	Path     string
	MetaData string
}

func fromPath(root string, relpath string) (content, error) {
	filepath := path.Join(root, relpath)
	info, err := os.Lstat(filepath)
	if err != nil {
		return content{}, err
	}

	f, err := os.Open(filepath)
	if err != nil {
		return content{}, err
	}
	defer f.Close()

	if info.IsDir() {
		children, err := f.Readdirnames(0)
		if err != nil {
			return content{}, err
		}
		name, found := bestReadme(children)
		if !found {
			return content{}, nil
		}
		relpath = path.Join(relpath, name)
		filepath = path.Join(root, relpath)
		f, err = os.Open(filepath)
		if err != nil {
			return content{}, err
		}
		defer f.Close()
	}

	buffer := make([]byte, 512)
	n, err := f.Read(buffer)
	if err != nil && err != io.EOF {
		return content{}, err
	}
	f.Seek(0, 0)

	contentType := http.DetectContentType(buffer[:n])

	html := template.HTML("<span>Preview Unavailable</span>")
	//TODO: test for individual types, and pretty-render text and preview images (others?)
	if strings.HasPrefix(contentType, "text") {
		data, err := ioutil.ReadAll(f)
		if err != nil {
			return content{}, err
		}
		escapeddata := template.HTMLEscapeString(string(data))
		//This is not a fast way to do this, but it shouldn't be a real concern in perspective
		html = template.HTML("<pre><code>" + escapeddata + "</code></pre>")
	}

	return content{Name: path.Base(relpath), Rendered: html, Path: relpath}, nil

}

type child struct {
	Name string
	info os.FileInfo
}

func (c child) IsDir() bool {
	return c.info.IsDir()
}

func (c child) SizeString() string {
	if c.info.IsDir() {
		return "-"
	}
	return humanize.Bytes(uint64(c.info.Size()))
}

func bestReadme(candidates []string) (string, bool) {
	//This is almost certainly over-engineering, but it's something to play with later
	var readmes []string
	for _, c := range candidates {
		if strings.HasPrefix(strings.ToUpper(c), "README") {
			readmes = append(readmes, c)
		}
	}

	if len(readmes) == 0 {
		return "", false
	}

	sort.Slice(readmes, func(i, j int) bool {
		return i < j
	})

	return readmes[len(readmes)-1], true
}

func isDir(path string) bool {
	stat, err := os.Lstat(path)
	if err != nil {
		//This may not always be correct, but all cases where that's so will error later and more accurately
		return false
	}
	return stat.IsDir()
}

func visitRawPaths(templates *template.Template, templateName string, repoName string, srcRoot string, dstRoot string) filepath.WalkFunc {
	return func(currentPath string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relUri := strings.TrimPrefix(currentPath, srcRoot)
		relUri = strings.TrimPrefix(relUri, "/")
		pageName := path.Base(relUri)
		if relUri == "" {
			pageName = "/"
		}
		contentPath := currentPath
		outPath := path.Join(dstRoot, relUri)
		outFileName := "index.html"
		var childnames []string
		var children []child
		var content content

		f, err := os.Open(contentPath)
		if err != nil {
			return err
		}
		defer f.Close()

		err = os.MkdirAll(outPath, 0777)
		if err != nil {
			return err
		}

		if info.IsDir() {
			childnames, err = f.Readdirnames(0)
			if err != nil {
				return err
			}

			children = make([]child, len(childnames))
			for i, c := range childnames {
				info, err := os.Lstat(path.Join(currentPath, c))
				if err != nil {
					return err
				}
				children[i] = child{Name: c, info: info}
			}
			sort.Slice(children, func(i, j int) bool {
				if children[i].IsDir() && !children[j].IsDir() {
					return true
				}
				if children[j].IsDir() && !children[i].IsDir() {
					return false
				}
				return children[i].Name < children[j].Name //TODO: consider case-insensitive sort
			})

			for _, c := range children {
				err = os.MkdirAll(path.Join(outPath, c.Name), 0777)
				if err != nil {
					return err
				}
			}
			if isDir(path.Join(outPath, outFileName)) {
				outFileName = "index.html.var"
				if isDir(path.Join(outPath, outFileName)) {
					return errors.New("Cannot create index for: " + relUri + ", conflicting files exist")
				}
			}
		}

		content, err = fromPath(srcRoot, relUri)
		if err != nil {
			return err
		}

		outFile, err := os.Create(path.Join(outPath, outFileName))
		if err != nil {
			return err
		}
		defer outFile.Close()

		context := pageContext{
			Project:  repoName,
			Name:     pageName,
			Content:  content,
			Children: children,
			Path:     pathwrapper(relUri),
		}
		err = templates.ExecuteTemplate(outFile, templateName, context)
		if err != nil {
			return err
		}
		return nil
	}
}

func buildRepoBrowse(name string, indir string, outdir string, tpath string, includes []string) error {
	templatename := filepath.Base(tpath)
	templates := template.New("")
	for _, d := range includes {
		_, err := templates.ParseGlob(d + "/*")
		if err != nil {
			return err
		}
	}
	templates.ParseFiles(tpath)

	finfo, err := os.Stat(indir)
	if err != nil {
		return err
	}
	if !finfo.IsDir() {
		return errors.New(fmt.Sprintf("%s must be a directory", indir))
	}

	return filepath.Walk(indir, visitRawPaths(templates, templatename, name, indir, outdir))
}

func main() {
	usage := fmt.Sprintf(`Builds static sites for source trees

Usage:
  %[1]s <name> [options] [--include=<dir>]...
  %[1]s (-h|--help)

Options:
  -h, --help                Show this message and exit
  -i DIR, --input DIR       Set the source directory to DIR [default: ./raw]
  -o DIR, --output DIR      Set the output directory to DIR [default: ./tree]
  -t FILE, --template FILE  Set the primary template [default: tree.tmpl]
  --include DIR             Include all files in DIR as templates
`, filepath.Base(os.Args[0]))

	arguments, err := docopt.Parse(usage, nil, true, "0.1.0", false)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	err = buildRepoBrowse(arguments["<name>"].(string),
		arguments["--input"].(string),
		arguments["--output"].(string),
		arguments["--template"].(string),
		arguments["--include"].([]string))
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	os.Exit(0)
}