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)
}