├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── all.go ├── auth.go ├── buildRoot ├── etc │ ├── bla │ │ ├── config.ini │ │ └── postinstall.sh │ ├── default │ │ └── bla │ └── logrotate.d │ │ └── bla ├── usr │ └── lib │ │ └── systemd │ │ └── system │ │ └── bla.service └── var │ ├── lib │ └── bla │ │ ├── docs │ │ └── hello-world.md │ │ ├── libs │ │ └── css │ │ │ └── base.css │ │ └── template │ │ ├── all.tmpl │ │ ├── doc.tmpl │ │ ├── footer.tmpl │ │ ├── header.tmpl │ │ ├── index.tmpl │ │ ├── root.tmpl │ │ ├── single.tmpl │ │ └── tag.tmpl │ └── log │ └── bla │ └── access.log ├── cmd └── bla │ └── main.go ├── config.go ├── doc.go ├── doc_test.go ├── error.go ├── index.go ├── link.go ├── main.go ├── server.go ├── sitemap.go ├── tags.go ├── tmpl.go └── webdav.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .tmpBuildRoot 3 | pkg 4 | vendor/.cache 5 | self.config.ini 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/github.com/beorn7/perks"] 2 | path = vendor/github.com/beorn7/perks 3 | url = https://github.com/beorn7/perks 4 | [submodule "vendor/github.com/fsnotify/fsnotify"] 5 | path = vendor/github.com/fsnotify/fsnotify 6 | url = https://github.com/fsnotify/fsnotify 7 | [submodule "vendor/github.com/golang/protobuf"] 8 | path = vendor/github.com/golang/protobuf 9 | url = https://github.com/golang/protobuf 10 | [submodule "vendor/github.com/matttproud/golang_protobuf_extensions"] 11 | path = vendor/github.com/matttproud/golang_protobuf_extensions 12 | url = https://github.com/matttproud/golang_protobuf_extensions 13 | [submodule "vendor/github.com/prometheus/client_golang"] 14 | path = vendor/github.com/prometheus/client_golang 15 | url = https://github.com/prometheus/client_golang 16 | [submodule "vendor/github.com/prometheus/client_model"] 17 | path = vendor/github.com/prometheus/client_model 18 | url = https://github.com/prometheus/client_model 19 | [submodule "vendor/github.com/prometheus/common"] 20 | path = vendor/github.com/prometheus/common 21 | url = https://github.com/prometheus/common 22 | [submodule "vendor/github.com/prometheus/procfs"] 23 | path = vendor/github.com/prometheus/procfs 24 | url = https://github.com/prometheus/procfs 25 | [submodule "vendor/github.com/russross/blackfriday"] 26 | path = vendor/github.com/russross/blackfriday 27 | url = https://github.com/russross/blackfriday 28 | [submodule "vendor/github.com/shurcooL/sanitized_anchor_name"] 29 | path = vendor/github.com/shurcooL/sanitized_anchor_name 30 | url = https://github.com/shurcooL/sanitized_anchor_name 31 | [submodule "vendor/golang.org/x/net"] 32 | path = vendor/golang.org/x/net 33 | url = https://go.googlesource.com/net 34 | [submodule "vendor/golang.org/x/sys"] 35 | path = vendor/golang.org/x/sys 36 | url = https://go.googlesource.com/sys 37 | [submodule "vendor/gopkg.in/ini.v1"] 38 | path = vendor/gopkg.in/ini.v1 39 | url = https://gopkg.in/ini.v1 40 | [submodule "vendor/github.com/lucas-clemente/quic-go"] 41 | path = vendor/github.com/lucas-clemente/quic-go 42 | url = https://github.com/lucas-clemente/quic-go 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Meng Zhuo 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell git describe --tags) 2 | DESTDIR?=.tmpBuildRoot 3 | 4 | .PHONY: binary 5 | binary: clean build 6 | 7 | .PHONY: clean 8 | clean: 9 | rm -rf *.deb 10 | rm -rf *.rpm 11 | rm -rf bla 12 | rm -rf ${DESTDIR} 13 | 14 | .PHONY: build 15 | build: 16 | go build -o bla -ldflags '-X main.Version=${VERSION}' cmd/bla/main.go 17 | 18 | .PHONY: pkg 19 | pkg: 20 | rm -rf ${DESTDIR} 21 | mkdir ${DESTDIR} 22 | cp -rf buildRoot/* ${DESTDIR}/ 23 | mkdir -p ${DESTDIR}/usr/local/bin 24 | mkdir -p ${DESTDIR}/var/log/bla/ 25 | cp bla ${DESTDIR}/usr/local/bin/ 26 | 27 | deb: clean build pkg 28 | fpm -t deb -s dir -n bla -v $(VERSION:v%=%) -C ${DESTDIR} --after-install buildRoot/etc/bla/postinstall.sh 29 | 30 | rpm: clean build pkg 31 | fpm -t rpm -s dir -n bla -v ${VERSION} -C ${DESTDIR} 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bla 2 | 3 | Blog/Lite-CMS build on Automatic static file serving 4 | 5 | ## feature 6 | 7 | * Markdown format, yes! 8 | * Tags and page 9 | * WebDav, no git/svn required 10 | * Automatically build site 11 | * Customable theme and everything 12 | * TLS/SSL ready 13 | 14 | ## TODO 15 | 16 | - [ ] HTTP jump to HTTPs 17 | - [x] RPM/Deb pack 18 | - [ ] default doc generate 19 | - [x] expvar 20 | - [x] service file 21 | - [x] sitemap.txt 22 | - [ ] detail document 23 | -------------------------------------------------------------------------------- /all.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func generateAllPage(s *Handler, publicPath string) (err error) { 9 | 10 | f, err := os.Create(filepath.Join(publicPath, "all")) 11 | if err != nil { 12 | return 13 | } 14 | defer f.Close() 15 | 16 | return s.tpl.ExecuteTemplate(f, "all", 17 | &mulDocData{s, "", s.sortDocs}) 18 | } 19 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | func fetchIP(ra string) string { 13 | 14 | i := strings.LastIndex(ra, ":") 15 | if i == -1 { 16 | return "unknown" 17 | } 18 | return ra[:i] 19 | } 20 | 21 | type authRateByIPHandler struct { 22 | origin http.Handler 23 | ticker *time.Ticker 24 | record map[string]int 25 | mu sync.RWMutex 26 | 27 | username, password string 28 | limit int 29 | realm string 30 | } 31 | 32 | func NewAuthRateByIPHandler(realm string, origin http.Handler, username, password string, limit int) *authRateByIPHandler { 33 | 34 | ticker := time.NewTicker(time.Minute) 35 | 36 | a := &authRateByIPHandler{origin, 37 | ticker, 38 | map[string]int{}, 39 | sync.RWMutex{}, 40 | 41 | username, 42 | password, 43 | limit, 44 | realm, 45 | } 46 | 47 | go func() { 48 | for { 49 | <-a.ticker.C 50 | a.mu.Lock() 51 | for k, _ := range a.record { 52 | delete(a.record, k) 53 | } 54 | a.mu.Unlock() 55 | } 56 | }() 57 | return a 58 | } 59 | 60 | func (a *authRateByIPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 61 | 62 | ip := fetchIP(r.RemoteAddr) 63 | a.mu.RLock() 64 | rec := a.record[ip] 65 | a.mu.RUnlock() 66 | if rec > a.limit { 67 | fmt.Fprintf(w, "

Too many request

") 68 | w.WriteHeader(429) 69 | return 70 | } 71 | 72 | w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="webfs@%s"`, a.realm)) 73 | if !a.checkAndLimit(w, r) { 74 | w.WriteHeader(401) 75 | return 76 | } 77 | a.origin.ServeHTTP(w, r) 78 | } 79 | 80 | func (a *authRateByIPHandler) checkAndLimit(w http.ResponseWriter, r *http.Request) (result bool) { 81 | 82 | s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) 83 | 84 | if len(s) != 2 { 85 | return 86 | } 87 | 88 | b, err := base64.StdEncoding.DecodeString(s[1]) 89 | if err != nil { 90 | return 91 | } 92 | 93 | pair := strings.SplitN(string(b), ":", 2) 94 | if len(pair) != 2 { 95 | return 96 | } 97 | 98 | result = (pair[0] == a.username && pair[1] == a.password) 99 | 100 | if !result { 101 | ip := fetchIP(r.RemoteAddr) 102 | a.mu.Lock() 103 | rec, ok := a.record[ip] 104 | if !ok { 105 | rec = 0 106 | } 107 | a.record[ip] = rec + 1 108 | a.mu.Unlock() 109 | } 110 | return result 111 | } 112 | -------------------------------------------------------------------------------- /buildRoot/etc/bla/config.ini: -------------------------------------------------------------------------------- 1 | RootPath=/var/lib/bla 2 | AccessLogPath=/var/log/bla/access.log 3 | MetricListenAddr= 4 | 5 | -------------------------------------------------------------------------------- /buildRoot/etc/bla/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | [ -f /etc/default/bla ] && . /etc/default/bla 6 | 7 | if [ -x /bin/systemctl ]; then 8 | systemctl daemon-reload 9 | systemctl restart bla 10 | fi 11 | -------------------------------------------------------------------------------- /buildRoot/etc/default/bla: -------------------------------------------------------------------------------- 1 | # Bla service environment files 2 | 3 | ARGS="-config /etc/bla/config.ini" 4 | 5 | # Usage of bla: 6 | # -config string 7 | # default config path (default "config.ini") 8 | -------------------------------------------------------------------------------- /buildRoot/etc/logrotate.d/bla: -------------------------------------------------------------------------------- 1 | /var/log/bla/*.log { 2 | rotate 365 3 | dateext 4 | daily 5 | delaycompress 6 | missingok 7 | notifempty 8 | copytruncate 9 | 10 | compressoptions -9 11 | } 12 | -------------------------------------------------------------------------------- /buildRoot/usr/lib/systemd/system/bla.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Bla blog service 3 | Documentation=https://github.com/mengzhuo/bla 4 | Wants=network.target 5 | After=network.target 6 | 7 | [Service] 8 | Type=simple 9 | EnvironmentFile=/etc/default/bla 10 | ExecStart=/usr/local/bin/bla $ARGS 11 | Restart=on-failure 12 | RestartSec=30 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | Alias=bla.service 17 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/docs/hello-world.md: -------------------------------------------------------------------------------- 1 | Title=Hello World 2 | Time=2009-12-27T14:03:05Z 3 | Tags=hello 4 | Public=true 5 | 6 | +++ 7 | 8 | Hello world! 9 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/libs/css/base.css: -------------------------------------------------------------------------------- 1 | body{color:#222;margin:0;padding:0;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height: 1.3em;} 2 | a {color:#375EAB;text-decoration:none;} 3 | a:hover {text-decoration: underline; } 4 | #topbar {background-color:rgb(224, 235, 245);padding:0.5em 0;text-align:center;min-width:385px;} 5 | #topbar:after {content:"";display:block;clear:both;} 6 | #topbar h1{margin:0.5em 0;font-size:1.5em;max-width:950px;text-align:left;float:left;} 7 | #topbar a{color:#222;} 8 | #topbar form{float:right;margin: 0.5em 0;} 9 | #topbar .menu {float:right;} 10 | #topbar .menu a{ padding: 5px 10px; float: left; margin: 0.5em; background: #375eab; color: #fff; border-radius: 5px; } 11 | #topbar .menu a:hover {text-decoration:none; box-shadow: 1px 1px 5px #111;} 12 | #topbar input{padding: 0.5em; border:#ccc 1px solid; border-radius: 4px;} 13 | .container{padding:0 1.5em;margin:0 auto;text-align:left;max-width:950px;} 14 | .container:after{content:"";display:block;clear:both;} 15 | article {text-align:left;border-bottom:1px solid #ddd;} 16 | p {word-wrap:break-word;max-width:800px;} 17 | ul {max-width:800px;} 18 | #sidebar { float: right; padding-left: 20px; width: 40%; max-width: 250px; background: #F3F3F3; margin: 20px 0 20px 20px; text-align:left; } 19 | #sidebar ul { padding: 0; } 20 | #sidebar li { list-style-type: none; } 21 | #site-footer {margin:3em 0 0;text-align:center;color:#666;} 22 | #site-footer p {max-width:100%;} 23 | .title {font-size:1.5em;margin:1em 0 0em;line-height:1em;} 24 | .editor {min-height:300px;padding-top:5px;} 25 | .editor .tag{border-bottom:1px dashed #444;} 26 | time {color:#888;font-size:0.6em;} 27 | .submit {padding:0.3em 0.5em;} 28 | pre, code {font-family:Consolas, 'Liberation Mono', Menlo, Courier, monospace;font-size:0.8em;} 29 | pre {background: #EFEFEF; padding: 10px; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; overflow:auto;} 30 | img {max-width:95% !important; height:auto;} 31 | blockquote {border-left:2px dashed #ccc;padding-left:1em;margin-left:1em;} 32 | h2,h3,h4 {color:#375EAB;} 33 | .related_header { font-size: 20px; background: #E0EBF5; padding: 8px; line-height: 1.25; font-weight: normal; } 34 | .tag-row {margin-top:10px;} 35 | .tag-row a {padding:3px 5px;border:1px solid #666;border-radius:5px;} 36 | @media screen and (max-width:624px){ 37 | #topbar {position: fixed; width: 100%; top: 0; box-shadow: 0 2px 5px #ccc;padding:0;} 38 | main {padding:3em 1em 1em !important;} 39 | } 40 | @media screen and (max-width:385px){#topbar form {display:none;}} 41 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/template/all.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "all" }} 2 | {{ template "header" . }} 3 | 15 | {{ template "footer" }} 16 | {{end}} 17 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/template/doc.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "doc" }} 2 |
3 |
4 |

{{ .Title }}

5 |
6 | 7 |
8 |
9 | {{ .Content }} 10 |
11 | {{end}} 12 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/template/footer.tmpl: -------------------------------------------------------------------------------- 1 | {{define "footer" }} 2 | 3 | 6 | 7 | 8 | {{end}} 9 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/template/header.tmpl: -------------------------------------------------------------------------------- 1 | {{define "header" }} 2 | 3 | 4 | 5 | {{ template "title" . }}{{ .Hdl.Cfg.Title }} 6 | 7 | 8 | 9 | 25 | 26 | 27 |
28 |
29 |

{{.Hdl.Cfg.Title}}

30 | 37 |
38 |
39 |
40 | {{end}} 41 | 42 | {{define "title" }}{{ if .Title }}{{.Title}} - {{end}}{{end}} 43 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/template/index.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "index" }} 2 | {{ template "header" . }} 3 | {{ range .Docs }} 4 | {{ template "doc" . }} 5 | {{ end }} 6 | {{ template "footer" }} 7 | {{end}} 8 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/template/root.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/template/single.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "single" }} 2 | {{ template "header" . }} 3 | {{ template "doc" .Doc }} 4 | 14 | {{ template "footer" }} 15 | {{end}} 16 | -------------------------------------------------------------------------------- /buildRoot/var/lib/bla/template/tag.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "tag_page" }} 2 | {{ template "header" . }} 3 | 12 | {{ template "footer" }} 13 | {{end}} 14 | -------------------------------------------------------------------------------- /buildRoot/var/log/bla/access.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengzhuo/bla/59710184e3581a5013b4d8810eab65a9f83fc19d/buildRoot/var/log/bla/access.log -------------------------------------------------------------------------------- /cmd/bla/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/mengzhuo/bla" 12 | ) 13 | 14 | const ( 15 | DefaultConfig = "config.ini" 16 | ) 17 | 18 | var ( 19 | configPath = flag.String("config", DefaultConfig, "default config path") 20 | varg = flag.Bool("v", false, "show version") 21 | Version = "dev" 22 | ) 23 | 24 | func main() { 25 | // defer profile.Start().Stop() 26 | flag.Parse() 27 | 28 | if *varg { 29 | fmt.Println("bla version:", Version) 30 | return 31 | } 32 | go listenToUSR1() 33 | bla.ListenAndServe(*configPath) 34 | } 35 | 36 | func listenToUSR1() { 37 | 38 | go func() { 39 | c := make(chan os.Signal, 1) 40 | signal.Notify(c, syscall.SIGUSR1) 41 | for range c { 42 | log.Print("got reload cert signal") 43 | bla.LoadCertificate() 44 | } 45 | }() 46 | } 47 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/user" 8 | "path/filepath" 9 | "strings" 10 | 11 | ini "gopkg.in/ini.v1" 12 | ) 13 | 14 | type Config struct { 15 | BaseURL string 16 | HostName string 17 | 18 | RootPath string 19 | LinkPath []string 20 | 21 | HomeDocCount int 22 | Title string 23 | UserName string 24 | Password string 25 | } 26 | 27 | func DefaultConfig() *Config { 28 | 29 | defaultLinks := []string{ 30 | "libs", 31 | } 32 | current, err := user.Current() 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | name := current.Username 37 | host, _ := os.Hostname() 38 | 39 | return &Config{ 40 | BaseURL: "", 41 | HostName: host, 42 | 43 | RootPath: "root", 44 | LinkPath: defaultLinks, 45 | 46 | HomeDocCount: 5, 47 | Title: fmt.Sprintf("%s's blog", strings.Title(name)), 48 | UserName: name, 49 | Password: "PLEASE_UPDATE_PASSWORD!", 50 | } 51 | } 52 | 53 | func loadConfig(h *Handler) { 54 | 55 | rawCfg, err := ini.Load(h.cfgPath) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | log.Print("loading config") 61 | cfg := DefaultConfig() 62 | 63 | rawCfg.MapTo(cfg) 64 | 65 | h.templatePath = filepath.Join(cfg.RootPath, "template") 66 | h.docPath = filepath.Join(cfg.RootPath, "docs") 67 | 68 | h.Cfg = cfg 69 | log.Printf("%#v", *cfg) 70 | 71 | } 72 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "sort" 13 | "time" 14 | 15 | "github.com/russross/blackfriday" 16 | 17 | ini "gopkg.in/ini.v1" 18 | ) 19 | 20 | const ( 21 | StatusPublic = "public" 22 | ) 23 | 24 | type Doc struct { 25 | Title string 26 | SlugTitle string 27 | Time time.Time 28 | ModTime time.Time 29 | Tags []string 30 | Public bool 31 | 32 | Content string 33 | } 34 | 35 | func newDoc(r io.Reader) (d *Doc, err error) { 36 | 37 | var buf []byte 38 | buf, err = ioutil.ReadAll(r) 39 | 40 | idx := bytes.Index(buf, []byte("+++")) 41 | if idx == -1 { 42 | return nil, fmt.Errorf("header not found") 43 | } 44 | 45 | d = &Doc{} 46 | sec, err := ini.Load(buf[:idx]) 47 | if err != nil { 48 | return 49 | } 50 | 51 | err = sec.MapTo(d) 52 | if err != nil { 53 | return 54 | } 55 | 56 | d.Content = string(blackfriday.MarkdownCommon(buf[idx+4:])) 57 | return 58 | } 59 | 60 | func generateSingle(s *Handler, pub string) (err error) { 61 | 62 | for slugTitle, doc := range s.docs { 63 | fp := filepath.Join(pub, slugTitle) 64 | f, err := os.Create(fp) 65 | if err != nil { 66 | return err 67 | } 68 | defer f.Close() 69 | 70 | err = s.tpl.ExecuteTemplate(f, "single", &singleData{s, doc.Title, doc}) 71 | if err != nil { 72 | return err 73 | } 74 | err = os.Chtimes(fp, time.Now(), doc.ModTime) 75 | if err != nil { 76 | // not a big deal... 77 | log.Print(err) 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | func loadData(s *Handler) { 84 | log.Print("Loading docs from:", s.docPath) 85 | 86 | s.mu.Lock() 87 | s.sortDocs = []*Doc{} 88 | s.docs = map[string]*Doc{} 89 | s.tags = map[string][]*Doc{} 90 | s.mu.Unlock() 91 | 92 | f, err := os.Open(s.docPath) 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | defer f.Close() 97 | 98 | err = filepath.Walk(s.docPath, s.docWalker) 99 | if err != nil { 100 | log.Print(err) 101 | } 102 | sort.Sort(docsByTime(s.sortDocs)) 103 | log.Print("Statistic docs....") 104 | log.Printf("Docs:%15d", len(s.docs)) 105 | log.Printf("Tags:%15d", len(s.tags)) 106 | } 107 | 108 | func (s *Handler) docWalker(p string, info os.FileInfo, err error) error { 109 | s.mu.Lock() 110 | defer s.mu.Unlock() 111 | 112 | //start := time.Now() 113 | if info.IsDir() || filepath.Ext(info.Name()) != ".md" { 114 | return nil 115 | } 116 | var f *os.File 117 | f, err = os.Open(p) 118 | if err != nil { 119 | return err 120 | } 121 | defer f.Close() 122 | 123 | var doc *Doc 124 | doc, err = newDoc(f) 125 | if err != nil { 126 | return err 127 | } 128 | if !doc.Public { 129 | log.Printf("doc:%s loaded but not public", p) 130 | return nil 131 | } 132 | 133 | doc.SlugTitle = path.Base(p)[0 : len(path.Base(p))-3] 134 | 135 | for _, t := range doc.Tags { 136 | s.tags[t] = append(s.tags[t], doc) 137 | } 138 | s.docs[doc.SlugTitle] = doc 139 | s.sortDocs = append(s.sortDocs, doc) 140 | /* 141 | log.Printf("loaded doc:%s in %s", doc.SlugTitle, 142 | time.Now().Sub(start).String()) 143 | */ 144 | doc.ModTime = info.ModTime() 145 | return nil 146 | } 147 | 148 | type docsByTime []*Doc 149 | 150 | func (s docsByTime) Len() int { return len(s) } 151 | func (s docsByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 152 | func (s docsByTime) Less(i, j int) bool { return s[i].Time.After(s[j].Time) } 153 | -------------------------------------------------------------------------------- /doc_test.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestNewRawDoc(t *testing.T) { 9 | ff := `Title=Hello World 10 | Time=2016-03-20T06:12:44Z 11 | Tags=golang, bla, epoch 12 | Public=true 13 | +++ 14 | ### Hohohoho 15 | ` 16 | doc, err := newDoc(strings.NewReader(ff)) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | t.Log(doc) 21 | t.Log(string(doc.Content)) 22 | } 23 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package bla 2 | -------------------------------------------------------------------------------- /index.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func generateIndex(s *Handler, public string) (err error) { 9 | 10 | var docs []*Doc 11 | if len(s.sortDocs) > s.Cfg.HomeDocCount { 12 | docs = s.sortDocs[:s.Cfg.HomeDocCount] 13 | } else { 14 | docs = s.sortDocs 15 | } 16 | 17 | f, err := os.Create(filepath.Join(public, "index.html")) 18 | if err != nil { 19 | return 20 | } 21 | defer f.Close() 22 | 23 | return s.tpl.ExecuteTemplate(f, "index", 24 | &mulDocData{s, "", docs}) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /link.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func wrapLinkToPublic(s *Handler, public string) filepath.WalkFunc { 11 | 12 | return func(path string, info os.FileInfo, err error) error { 13 | if path == s.Cfg.RootPath { 14 | return nil 15 | } 16 | 17 | if strings.Count(path, "/")-strings.Count(s.Cfg.RootPath, "/") > 1 { 18 | // not base dir skip it... 19 | return filepath.SkipDir 20 | } 21 | 22 | switch base := filepath.Base(path); base { 23 | case "template", "docs", ".public", "certs": 24 | return nil 25 | default: 26 | realPath, err := filepath.Abs(path) 27 | if err != nil { 28 | return err 29 | } 30 | target := filepath.Join(public, base) 31 | log.Printf("link %s -> %s", realPath, target) 32 | 33 | err = os.Symlink(realPath, target) 34 | if err != nil { 35 | if os.IsExist(err) { 36 | return nil 37 | } 38 | log.Fatal(err) 39 | } 40 | } 41 | return nil 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "sync" 12 | "text/template" 13 | "time" 14 | 15 | "github.com/fsnotify/fsnotify" 16 | ) 17 | 18 | const ( 19 | Version = "dev" 20 | ) 21 | 22 | // Initialize 23 | type Handler struct { 24 | cfgPath string 25 | Cfg *Config 26 | public http.Handler 27 | webfs http.Handler 28 | tpl *template.Template 29 | 30 | publicPath string 31 | templatePath string 32 | docPath string 33 | 34 | mu sync.RWMutex 35 | docs map[string]*Doc 36 | sortDocs []*Doc 37 | tags map[string][]*Doc 38 | } 39 | 40 | func NewHandler(cfgPath string) *Handler { 41 | 42 | h := &Handler{ 43 | cfgPath: cfgPath, 44 | mu: sync.RWMutex{}, 45 | } 46 | 47 | loadConfig(h) 48 | h.watch() 49 | loadWebDav(h) 50 | 51 | return h 52 | } 53 | 54 | func (s *Handler) watch() { 55 | 56 | watcher, err := fsnotify.NewWatcher() 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | go func() { 62 | // init all docs 63 | loadData(s) 64 | loadTemplate(s) 65 | err := saveAll(s) 66 | if err != nil { 67 | log.Fatal("can't save docs:", err) 68 | } 69 | // loadData minial interval is 1 second 70 | ticker := time.NewTicker(time.Second) 71 | docChange := false 72 | rootChange := false 73 | 74 | for { 75 | 76 | select { 77 | case event := <-watcher.Events: 78 | switch ext := filepath.Ext(event.Name); ext { 79 | case ".md", ".tmpl": 80 | if strings.HasPrefix(event.Name, ".") { 81 | continue 82 | } 83 | log.Println("modified file:", event.Name) 84 | docChange = true 85 | case ".swp": 86 | continue 87 | } 88 | 89 | rootChange = true 90 | case err := <-watcher.Errors: 91 | log.Println("error:", err) 92 | case <-ticker.C: 93 | if docChange { 94 | docChange = false 95 | loadData(s) 96 | loadTemplate(s) 97 | } 98 | 99 | if rootChange { 100 | rootChange = false 101 | err := saveAll(s) 102 | if err != nil { 103 | log.Print("can't save docs:", err) 104 | continue 105 | } 106 | } 107 | } 108 | } 109 | }() 110 | 111 | watcher.Add(s.Cfg.RootPath) 112 | watcher.Add(s.docPath) 113 | watcher.Add(s.templatePath) 114 | 115 | if err != nil { 116 | log.Print(err) 117 | } 118 | } 119 | 120 | func clearOldTmp(exclude string) (err error) { 121 | realExcluded, err := filepath.Abs(exclude) 122 | if err != nil { 123 | return err 124 | } 125 | old, err := filepath.Glob(filepath.Join(os.TempDir(), "bla_*")) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | for _, path := range old { 131 | realPath, err := filepath.Abs(path) 132 | if err != nil { 133 | return err 134 | } 135 | if realPath == realExcluded { 136 | continue 137 | } 138 | log.Println("removing old public", realPath) 139 | os.RemoveAll(realPath) 140 | } 141 | return nil 142 | } 143 | 144 | type handleFunc func(s *Handler, public string) error 145 | 146 | func saveAll(s *Handler) (err error) { 147 | 148 | var newPub string 149 | newPub, err = ioutil.TempDir("", fmt.Sprintf("bla_%s_", s.Cfg.UserName)) 150 | if err != nil { 151 | return err 152 | } 153 | s.mu.Lock() 154 | defer s.mu.Unlock() 155 | 156 | for k, function := range map[string]handleFunc{ 157 | "index": generateIndex, 158 | "all_page": generateAllPage, 159 | "sitemap": generateSiteMap, 160 | "tag": generateTagPage, 161 | "docs": generateSingle, 162 | } { 163 | start := time.Now() 164 | err = function(s, newPub) 165 | cost := time.Now().Sub(start) 166 | if err != nil { 167 | log.Printf("generate:%-15s ... [ERR]", k) 168 | return err 169 | } 170 | log.Printf("generate:%-15s ... [OK][%s]", k, cost) 171 | } 172 | 173 | filepath.Walk(s.Cfg.RootPath, wrapLinkToPublic(s, newPub)) 174 | 175 | s.publicPath = newPub 176 | s.public = http.FileServer(http.Dir(s.publicPath)) 177 | clearOldTmp(s.publicPath) 178 | return nil 179 | } 180 | 181 | func (s *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 182 | 183 | if strings.HasPrefix(r.URL.Path, "/fs") { 184 | s.webfs.ServeHTTP(w, r) 185 | httpRequestCount.WithLabelValues("/fs").Inc() 186 | } else { 187 | s.public.ServeHTTP(w, r) 188 | httpRequestCount.WithLabelValues("/doc").Inc() 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/lucas-clemente/quic-go/h2quic" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | 17 | ini "gopkg.in/ini.v1" 18 | ) 19 | 20 | var ( 21 | logPool = sync.Pool{New: func() interface{} { return &LogWriter{nil, 200} }} 22 | 23 | tlsCert *tls.Certificate 24 | server *http.Server 25 | cfg *ServerConfig 26 | 27 | httpRequestCount = prometheus.NewCounterVec( 28 | prometheus.CounterOpts{ 29 | Namespace: "http", 30 | Subsystem: "requests", 31 | Name: "total", 32 | Help: "The total number of http request", 33 | }, 34 | []string{"handler"}) 35 | 36 | httpRequestDurationSeconds = prometheus.NewSummary( 37 | prometheus.SummaryOpts{ 38 | Namespace: "http", 39 | Subsystem: "request", 40 | Name: "duration_seconds", 41 | Help: "The request duration distribution", 42 | }) 43 | ) 44 | 45 | func listenMetric(addr string) { 46 | log.Printf("prometheus metric at %s/%s", addr, "metrics") 47 | prometheus.MustRegister(httpRequestCount) 48 | prometheus.MustRegister(httpRequestDurationSeconds) 49 | http.Handle("/metrics", promhttp.Handler()) 50 | http.ListenAndServe(addr, nil) 51 | } 52 | 53 | // Config --------------------- 54 | 55 | type ServerConfig struct { 56 | Certfile string 57 | Keyfile string 58 | Listen string 59 | MetricListenAddr string 60 | AccessLogPath string 61 | } 62 | 63 | func ListenAndServe(cfgPath string) { 64 | raw, err := ini.Load(cfgPath) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | 69 | cfg = &ServerConfig{ 70 | "", "", 71 | ":8080", 72 | "", 73 | "access.log", 74 | } 75 | 76 | raw.MapTo(cfg) 77 | 78 | log.Printf("pid:%d", os.Getpid()) 79 | 80 | if cfg.MetricListenAddr != "" { 81 | go listenMetric(cfg.MetricListenAddr) 82 | } 83 | 84 | log.Printf("Server:%v", cfg) 85 | 86 | h := NewHandler(cfgPath) 87 | lh := logTimeAndStatus(cfg, h) 88 | server := &http.Server{Handler: lh, Addr: cfg.Listen} 89 | 90 | if cfg.Certfile != "" && cfg.Keyfile != "" { 91 | LoadCertificate() 92 | quic := &h2quic.Server{Server: server} 93 | quic.TLSConfig = &tls.Config{} 94 | quic.TLSConfig.GetCertificate = getCertificate 95 | 96 | pln, err := net.ListenPacket("udp", cfg.Listen) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | log.Print("listen quic on udp:%s", cfg.Listen) 101 | go quic.Serve(pln) 102 | 103 | // for higher score in ssllab 104 | log.Fatal(server.ListenAndServeTLS(cfg.Certfile, cfg.Keyfile)) 105 | } 106 | server.ListenAndServe() 107 | } 108 | 109 | type LogWriter struct { 110 | http.ResponseWriter 111 | statusCode int 112 | } 113 | 114 | func (l *LogWriter) WriteHeader(i int) { 115 | l.statusCode = i 116 | l.ResponseWriter.WriteHeader(i) 117 | 118 | } 119 | func logTimeAndStatus(cfg *ServerConfig, handler http.Handler) http.Handler { 120 | 121 | var ( 122 | writer io.Writer 123 | err error 124 | ) 125 | 126 | if cfg.AccessLogPath != "" { 127 | var file *os.File 128 | file, err = os.OpenFile(cfg.AccessLogPath, 129 | os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | writer = file 134 | log.Printf("Access Log to file: %s", cfg.AccessLogPath) 135 | file.Seek(0, os.SEEK_END) 136 | } else { 137 | writer = os.Stdout 138 | } 139 | 140 | accessLogger := log.New(writer, "", log.LstdFlags) 141 | 142 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 143 | start := time.Now() 144 | 145 | writer := logPool.Get().(*LogWriter) 146 | writer.ResponseWriter = w 147 | writer.statusCode = 200 148 | 149 | if cfg.Certfile != "" { 150 | writer.ResponseWriter.Header().Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") 151 | writer.ResponseWriter.Header().Add("alt-svc", `quic=":443"; ma=2592000; v="38,37,36"`) 152 | // alt-svc:quic=":443"; ma=2592000; v="39,38,37,35" 153 | } 154 | handler.ServeHTTP(writer, r) 155 | 156 | delta := time.Since(start) 157 | 158 | accessLogger.Printf(`%s %s %s %s %d "%s"`, 159 | r.RemoteAddr, r.Method, r.URL.Path, 160 | delta, writer.statusCode, r.UserAgent()) 161 | 162 | httpRequestDurationSeconds.Observe(delta.Seconds()) 163 | 164 | logPool.Put(writer) 165 | }) 166 | } 167 | 168 | func LoadCertificate() { 169 | 170 | log.Println("Loading new certs") 171 | cert, err := tls.LoadX509KeyPair(cfg.Certfile, cfg.Keyfile) 172 | if err != nil { 173 | log.Println("load cert failed keep old", err) 174 | return 175 | } 176 | tlsCert = &cert 177 | } 178 | 179 | func getCertificate(ch *tls.ClientHelloInfo) (cert *tls.Certificate, err error) { 180 | return tlsCert, nil 181 | } 182 | -------------------------------------------------------------------------------- /sitemap.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | ) 11 | 12 | func generateSiteMap(h *Handler, public string) (err error) { 13 | 14 | buf := bytes.NewBuffer(nil) 15 | for _, d := range h.sortDocs { 16 | fmt.Fprintf(buf, "%s%s/%s\n", "https://", h.Cfg.HostName, 17 | path.Join(h.Cfg.BaseURL, d.SlugTitle)) 18 | } 19 | 20 | err = ioutil.WriteFile(filepath.Join(public, "sitemap.txt"), 21 | buf.Bytes(), os.ModePerm) 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "sort" 7 | ) 8 | 9 | func generateTagPage(s *Handler, publicPath string) (err error) { 10 | 11 | err = os.MkdirAll(filepath.Join(publicPath, "tags"), 0700) 12 | if err != nil { 13 | return 14 | } 15 | 16 | for tagName, docs := range s.tags { 17 | err = makeTagPage(s, publicPath, tagName, docs) 18 | if err != nil { 19 | return 20 | } 21 | } 22 | return nil 23 | } 24 | 25 | func makeTagPage(s *Handler, pub string, tagName string, docs []*Doc) (err error) { 26 | var f *os.File 27 | 28 | f, err = os.Create(filepath.Join(pub, "/tags/", tagName)) 29 | if err != nil { 30 | return 31 | } 32 | defer f.Close() 33 | sort.Sort(docsByTime(docs)) 34 | err = s.tpl.ExecuteTemplate(f, "tag_page", 35 | &tagData{s, tagName, docs, tagName}) 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /tmpl.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import ( 4 | "log" 5 | "text/template" 6 | ) 7 | 8 | func loadTemplate(s *Handler) { 9 | s.mu.Lock() 10 | defer s.mu.Unlock() 11 | 12 | log.Printf("loding template:%s", s.templatePath) 13 | tpl, err := template.ParseGlob(s.templatePath + "/*.tmpl") 14 | if err != nil { 15 | log.Print(err) 16 | return 17 | } 18 | s.tpl = tpl 19 | } 20 | 21 | type mulDocData struct { 22 | Hdl *Handler 23 | Title string 24 | Docs []*Doc 25 | } 26 | 27 | type singleData struct { 28 | Hdl *Handler 29 | Title string 30 | Doc *Doc 31 | } 32 | 33 | type tagData struct { 34 | Hdl *Handler 35 | Title string 36 | Docs []*Doc 37 | TagName string 38 | } 39 | -------------------------------------------------------------------------------- /webdav.go: -------------------------------------------------------------------------------- 1 | package bla 2 | 3 | import "golang.org/x/net/webdav" 4 | 5 | func loadWebDav(s *Handler) { 6 | 7 | fs := webdav.Dir(s.Cfg.RootPath) 8 | ls := webdav.NewMemLS() 9 | 10 | handler := &webdav.Handler{ 11 | Prefix: "/fs", 12 | FileSystem: fs, 13 | LockSystem: ls, 14 | } 15 | a := NewAuthRateByIPHandler(s.Cfg.HostName, handler, s.Cfg.UserName, 16 | s.Cfg.Password, 3) 17 | s.webfs = a 18 | } 19 | --------------------------------------------------------------------------------