├── error.go ├── .gitignore ├── all.go ├── doc_test.go ├── webdav.go ├── README.md ├── sitemap.go ├── index.go ├── tmpl.go ├── Makefile ├── tags.go ├── cmd └── bla │ └── main.go ├── link.go ├── LICENSE ├── config.go ├── .gitmodules ├── auth.go ├── doc.go ├── main.go └── server.go /error.go: -------------------------------------------------------------------------------- 1 | package bla 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .tmpBuildRoot 3 | pkg 4 | vendor/.cache 5 | self.config.ini 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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, "