├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── git.go ├── handlers.go ├── html.go ├── main.go ├── middleware.go ├── templates └── base.html └── wiki.go /.gitignore: -------------------------------------------------------------------------------- 1 | /gowiki* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Peter Renström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Wiki 2 | 3 | A simple HTTP server rendering Markdown styled documents on the fly and optionally shows its git history including diffs. 4 | 5 | **NOTE** This is toy project to help me learn Go, so don't run this on anything publically available. 6 | 7 | ![Screenshot1](https://cloud.githubusercontent.com/assets/177685/5720761/2337178e-9b29-11e4-8a86-224f7905b3f6.png) 8 | 9 | ## Installation 10 | 11 | ```bash 12 | $ go get github.com/renstrom/go-wiki 13 | $ $GOPATH/bin/go-wiki 14 | ``` 15 | 16 | ## Customize 17 | 18 | It's only possible to customize the CSS. Put all your customizations in a file of your choosing and point to it using the `--custom-css` flag. 19 | 20 | ```bash 21 | $ go-wiki ~/www/wiki --custom-css= 22 | ``` 23 | 24 | ## Usage 25 | 26 | Create git repository containing your Markdown formatted wiki pages. 27 | 28 | ### On the server 29 | 30 | Create an empty repository. 31 | 32 | ``` bash 33 | $ mkdir -p ~/www/wiki && cd $_ 34 | $ git init 35 | $ git config core.worktree ~/www/wiki 36 | $ git config receive.denycurrentbranch ignore 37 | ``` 38 | 39 | Setup a post-receive hook. 40 | 41 | ``` bash 42 | $ cat > .git/hooks/post-receive <` and `` with credentials for your specific machine. 58 | 59 | ``` bash 60 | $ git init 61 | $ git remote add origin \ 62 | ssh://@/home//www/wiki 63 | ``` 64 | 65 | Now create some Markdown file and push. 66 | 67 | ``` bash 68 | $ git add index.md 69 | $ git commit -m 'Add index page' 70 | $ git push origin master 71 | ``` 72 | 73 | ## License 74 | 75 | MIT 76 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/shurcooL/github_flavored_markdown" 9 | ) 10 | 11 | func DiffHandler(w http.ResponseWriter, r *http.Request) { 12 | parts := strings.Split(r.URL.Path[len("/api/diff/"):], "/") 13 | if len(parts) != 2 { 14 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 15 | } 16 | 17 | hash := parts[0] 18 | file := parts[1] + ".md" 19 | 20 | diff, err := Diff(file, hash) 21 | if err != nil { 22 | log.Println("ERROR", "Failed to get commit hash", hash) 23 | } 24 | 25 | // XXX: This could probably be done in a nicer way 26 | wrappedDiff := []byte("```diff\n" + string(diff) + "```") 27 | // md := blackfriday.MarkdownCommon(wrappedDiff) 28 | md := github_flavored_markdown.Markdown(wrappedDiff) 29 | 30 | w.Header().Set("Content-Type", "text/html") 31 | w.Write(md) 32 | } 33 | -------------------------------------------------------------------------------- /git.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "log" 8 | "os/exec" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type Commit struct { 16 | Author string 17 | Date time.Time 18 | File string 19 | Hash string 20 | Subject string 21 | } 22 | 23 | func (c Commit) Diff() ([]byte, error) { 24 | return Diff(c.File, c.Hash) 25 | } 26 | 27 | func (c Commit) FileNoExt() string { 28 | return strings.TrimSuffix(c.File, filepath.Ext(c.File)) 29 | } 30 | 31 | func (c Commit) HumanDate() string { 32 | return c.Date.Format("2006-01-02 15:04") 33 | } 34 | 35 | func Diff(file, hash string) ([]byte, error) { 36 | var out bytes.Buffer 37 | 38 | git := exec.Command("git", "-C", options.Dir, "show", "--oneline", "--no-color", hash, file) 39 | 40 | // Prune diff stats from output with tail 41 | tail := exec.Command("tail", "-n", "+8") 42 | 43 | var err error 44 | tail.Stdin, err = git.StdoutPipe() 45 | if err != nil { 46 | log.Println("ERROR", err) 47 | } 48 | 49 | tail.Stdout = &out 50 | 51 | err = tail.Start() 52 | if err != nil { 53 | log.Println("ERROR", err) 54 | } 55 | 56 | err = git.Run() 57 | if err != nil { 58 | log.Println("ERROR", err) 59 | } 60 | 61 | err = tail.Wait() 62 | if err != nil { 63 | log.Println("ERROR", err) 64 | } 65 | 66 | return out.Bytes(), err 67 | } 68 | 69 | func Commits(filename string, n int) ([]Commit, error) { 70 | var commits []Commit 71 | 72 | // abbreviated commit hash|author name|author date, UNIX timestamp|subject 73 | logFormat := "--pretty=%h|%an|%at|%s" 74 | 75 | cmd := exec.Command("git", "-C", options.Dir, "log", "-n", strconv.Itoa(n), logFormat, filename) 76 | stdout, err := cmd.StdoutPipe() 77 | if err != nil { 78 | log.Println("ERROR", err) 79 | return commits, err 80 | } 81 | 82 | defer stdout.Close() 83 | 84 | err = cmd.Start() 85 | if err != nil { 86 | log.Println("ERROR", err) 87 | return commits, err 88 | } 89 | 90 | out := bufio.NewScanner(stdout) 91 | for out.Scan() { 92 | fields := strings.Split(out.Text(), "|") 93 | 94 | commit := Commit{ 95 | Author: fields[1], 96 | File: filename, 97 | Hash: fields[0], 98 | Subject: fields[3], 99 | } 100 | 101 | unix, err := strconv.ParseInt(fields[2], 10, 64) 102 | if err != nil { 103 | log.Println("ERROR", err) 104 | } 105 | commit.Date = time.Unix(unix, 0) 106 | 107 | commits = append(commits, commit) 108 | } 109 | 110 | return commits, nil 111 | } 112 | 113 | // Check if a path contains a Git repository 114 | func IsGitRepository(path string) bool { 115 | var out bytes.Buffer 116 | cmd := exec.Command("git", "-C", options.Dir, "rev-parse", "--is-inside-work-tree") 117 | cmd.Stdout = &out 118 | 119 | err := cmd.Run() 120 | if err != nil { 121 | log.Println("ERROR", err) 122 | return false 123 | } 124 | 125 | var val bool 126 | _, err = fmt.Sscanf(out.String(), "%t", &val) 127 | if err != nil { 128 | log.Println("ERROR", err) 129 | return false 130 | } 131 | 132 | return val 133 | } 134 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | const imageTypes = ".jpg .jpeg .png .gif" 13 | 14 | func WikiHandler(w http.ResponseWriter, r *http.Request) { 15 | filePath := r.URL.Path[1:] 16 | if filePath == "" { 17 | filePath = "index" 18 | } 19 | 20 | // Deny requests trying to traverse up the directory structure using 21 | // relative paths 22 | if strings.Contains(filePath, "..") { 23 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 24 | return 25 | } 26 | 27 | // Path to the file as it is on the the local file system 28 | fsPath := fmt.Sprintf("%s/%s", options.Dir, filePath) 29 | 30 | // Serve (accepted) images 31 | for _, filext := range strings.Split(imageTypes, " ") { 32 | if path.Ext(r.URL.Path) == filext { 33 | http.ServeFile(w, r, fsPath) 34 | return 35 | } 36 | } 37 | 38 | // Serve custom CSS 39 | if options.CustomCSS != "" && r.URL.Path == "/css/custom.css" { 40 | http.ServeFile(w, r, options.CustomCSS) 41 | return 42 | } 43 | 44 | md, err := ioutil.ReadFile(fsPath + ".md") 45 | if err != nil { 46 | http.NotFound(w, r) 47 | return 48 | } 49 | 50 | wiki := Wiki{ 51 | Markdown: md, 52 | CustomCSS: options.CustomCSS, 53 | filepath: fsPath, 54 | template: options.template, 55 | } 56 | 57 | wiki.Commits, err = Commits(filePath+".md", 5) 58 | if err != nil { 59 | log.Println("ERROR", "Failed to get commits") 60 | } 61 | 62 | wiki.Write(w) 63 | } 64 | -------------------------------------------------------------------------------- /html.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const Template = ` 4 | 5 | 6 | Wiki 7 | 8 | 9 | 76 | 77 | {{ if .CustomCSS }} 78 | 79 | {{ end }} 80 | 81 | 82 | 83 |
{{ .Title }}
84 | 85 |
{{ .Body }}
86 | 87 |
88 | 89 |
90 | {{ range .Commits }} 91 |
92 | {{ .HumanDate }} · 93 | {{ .Author }} · 94 | {{ .Subject }} 95 |
96 |
97 | {{ end}} 98 |
99 | 100 | 148 | 149 | ` 150 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | flag "github.com/ogier/pflag" 12 | ) 13 | 14 | const Usage = `Usage: gowiki [options...] 15 | 16 | Positional arguments: 17 | path directory to serve wiki pages from 18 | 19 | Optional arguments: 20 | -h, --help show this help message and exit 21 | -p PORT, --port=PORT listen port (default 8080) 22 | --custom-css=PATH path to custom CSS file 23 | ` 24 | 25 | var options struct { 26 | Dir string 27 | Port int 28 | CustomCSS string 29 | 30 | template *template.Template 31 | git bool 32 | } 33 | 34 | func main() { 35 | flag.Usage = func() { 36 | fmt.Fprint(os.Stderr, Usage) 37 | } 38 | 39 | flag.IntVarP(&options.Port, "port", "p", 8080, "") 40 | flag.StringVar(&options.CustomCSS, "custom-css", "", "") 41 | 42 | flag.Parse() 43 | 44 | options.Dir = flag.Arg(0) 45 | 46 | if options.Dir == "" { 47 | flag.Usage() 48 | os.Exit(1) 49 | } 50 | 51 | log.Println("Serving wiki from", options.Dir) 52 | 53 | // Parse base template 54 | var err error 55 | options.template, err = template.New("base").Parse(Template) 56 | if err != nil { 57 | log.Fatalln("Error parsing HTML template:", err) 58 | } 59 | 60 | // Trim trailing slash from root path 61 | if strings.HasSuffix(options.Dir, "/") { 62 | options.Dir = options.Dir[:len(options.Dir)-1] 63 | } 64 | 65 | // Verify that the wiki folder exists 66 | _, err = os.Stat(options.Dir) 67 | if os.IsNotExist(err) { 68 | log.Fatalln("Directory not found") 69 | } 70 | 71 | // Check if the wiki folder is a Git repository 72 | options.git = IsGitRepository(options.Dir) 73 | if options.git { 74 | log.Println("Git repository found in directory") 75 | } else { 76 | log.Println("No git repository found in directory") 77 | } 78 | 79 | http.Handle("/api/diff/", commonHandler(DiffHandler)) 80 | http.Handle("/", commonHandler(WikiHandler)) 81 | 82 | log.Println("Listening on:", options.Port) 83 | http.ListenAndServe(fmt.Sprintf(":%d", options.Port), nil) 84 | } 85 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | func commonHandler(next http.HandlerFunc) http.Handler { 10 | fn := func(w http.ResponseWriter, r *http.Request) { 11 | defer func() { 12 | err := recover() 13 | if err != nil { 14 | log.Printf("panic: %+v", err) 15 | http.Error(w, http.StatusText(http.StatusInternalServerError), 16 | http.StatusInternalServerError) 17 | } 18 | }() 19 | 20 | t0 := time.Now() 21 | next.ServeHTTP(w, r) 22 | log.Printf("[%s] %q %v", r.Method, r.URL.String(), time.Now().Sub(t0)) 23 | } 24 | 25 | return http.HandlerFunc(fn) 26 | } 27 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wiki 5 | 6 | 7 | 8 | 75 | 76 | 77 |
{{ .Title }}
78 | 79 |
{{ .Body }}
80 | 81 |
82 | 83 |
84 | {{ range .Commits }} 85 |
86 | {{ .HumanDate }} · 87 | {{ .Author }} · 88 | {{ .Subject }} 89 |
90 |
91 | {{ end}} 92 |
93 | 94 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /wiki.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | "path" 7 | "strings" 8 | 9 | "github.com/russross/blackfriday" 10 | ) 11 | 12 | type Wiki struct { 13 | Body template.HTML 14 | Markdown []byte 15 | Commits []Commit 16 | CustomCSS string 17 | 18 | template *template.Template 19 | filepath string 20 | } 21 | 22 | func (w Wiki) Title() string { 23 | _, file := path.Split(w.filepath) 24 | file = strings.Replace(file, "_", " ", -1) 25 | file = strings.Title(file) 26 | return file 27 | } 28 | 29 | func (w *Wiki) Write(rw http.ResponseWriter) { 30 | w.Body = template.HTML(blackfriday.MarkdownCommon(w.Markdown)) 31 | err := w.template.Execute(rw, w) 32 | if err != nil { 33 | http.Error(rw, err.Error(), http.StatusInternalServerError) 34 | } 35 | } 36 | --------------------------------------------------------------------------------