diff --git a/caddyhugo.go b/caddyhugo.go index 33cd523..fbb3879 100644 --- a/caddyhugo.go +++ b/caddyhugo.go @@ -1,63 +1,43 @@ package caddyhugo import ( - "encoding/base64" - "errors" "fmt" "html/template" - "io/ioutil" "net/http" _ "net/http/pprof" - "os" "os/exec" - "path" - "path/filepath" "strings" "sync" - "time" "git.stephensearles.com/stephen/acedoc" - "git.stephensearles.com/stephen/caddy-hugo2/assets" "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugolib" - "github.com/gorilla/websocket" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" "github.com/spf13/afero" ) -const ( - IdleWebsocketTimeout = 10 * time.Minute - WebsocketFileTicker = 1 * time.Second -) - func init() { plugin := CaddyHugo{} // register a "generic" plugin, like a directive or middleware caddy.RegisterPlugin("hugo", caddy.Plugin{ ServerType: "http", - Action: plugin.Setup, + Action: plugin.SetupCaddy, }) // ... there are others. See the godoc. } -type docref struct { - clients uint - name string - doc *acedoc.Document - tmpdir string -} - type CaddyHugo struct { ServerType string Site *httpserver.SiteConfig HugoSites *hugolib.HugoSites HugoCfg *deps.DepsCfg + Dir string + Media *MediaSource docs map[string]*docref @@ -68,24 +48,6 @@ type CaddyHugo struct { ltime uint64 } -func (ch *CaddyHugo) ObserveLTime(ltime uint64) uint64 { - ch.mtx.Lock() - - if ch.ltime < ltime { - ch.ltime = ltime - } - - ch.mtx.Unlock() - return ch.LTime() -} - -func (ch *CaddyHugo) LTime() uint64 { - ch.mtx.Lock() - defer ch.mtx.Unlock() - ch.ltime++ - return ch.ltime -} - func (ch *CaddyHugo) Build() error { err := ch.HugoSites.Build(hugolib.BuildCfg{ResetState: true}) if err != nil { @@ -95,342 +57,17 @@ func (ch *CaddyHugo) Build() error { return nil } -func (ch *CaddyHugo) Setup(c *caddy.Controller) error { - var err error - - ch.docs = make(map[string]*docref) - ch.Site = httpserver.GetConfig(c) - - ch.HugoCfg = &deps.DepsCfg{} - - ch.HugoCfg.Cfg, err = hugolib.LoadConfig(hugofs.Os, ch.Site.Root, "") - if err != nil { - return fmt.Errorf("error loading hugo config: %v", err) - } - - ch.HugoCfg.Cfg.Set("workingdir", ch.Site.Root) - ch.HugoSites, err = hugolib.NewHugoSites(*ch.HugoCfg) - if err != nil { - return fmt.Errorf("error intializing hugo: %v", err) - } - - err = ch.Build() - if err != nil { - return fmt.Errorf("error building initial hugo: %v", err) - } - - ch.authorTmpl, err = template.New("").Parse(AuthorPage) - if err != nil { - return fmt.Errorf("author template invalid: %v", err) - } - - ch.adminTmpl, err = template.New("").Parse(AdminPage) - if err != nil { - return fmt.Errorf("admin template invalid: %v", err) - } - - ch.editTmpl, err = template.New("").Parse(EditPage) - if err != nil { - return fmt.Errorf("edit template invalid: %v", err) - } - - thumbDir, err := ioutil.TempDir("", "thumbs") - if err != nil { - return fmt.Errorf("couldn't initialize media: %v", err) - } - - ch.Media = &MediaSource{ - StorageDir: path.Join(ch.Site.Root, "media"), - ThumbDir: thumbDir, - } - - err = os.MkdirAll(ch.Media.StorageDir, 0755) - if err != nil { - return fmt.Errorf("couldn't initialize media: %v", err) - } - - // add a function that wraps listeners for the HTTP server - // (it's more common for a directive to call this rather than a standalone plugin) - ch.Site.AddMiddleware(ch.Middleware(c)) - - return ch.Publish() -} - -func (ch *CaddyHugo) ServeHTTPWithNext(next httpserver.Handler, c *caddy.Controller, w http.ResponseWriter, r *http.Request) (int, error) { - if !ch.Match(r) { - p := path.Join(ch.Site.Root, "public", r.URL.Path) - http.ServeFile(w, r, p) - return 200, nil - } - - if !ch.Auth(r) { - return http.StatusUnauthorized, errors.New("not authorized") - } - - if strings.HasPrefix(r.URL.Path, "/hugo/publish") { - err := ch.Publish() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return http.StatusInternalServerError, nil - } - - http.Redirect(w, r, "/", http.StatusFound) - return http.StatusFound, nil - } - if strings.HasPrefix(r.URL.Path, "/hugo/simplemde.css") { - w.Write(assets.MustAsset("simplemde/dist/simplemde.min.css")) - return http.StatusOK, nil - } - if strings.HasPrefix(r.URL.Path, "/hugo/simplemde.js") { - w.Write(assets.MustAsset("simplemde/debug/simplemde.js")) - return http.StatusOK, nil - } - if strings.HasPrefix(r.URL.Path, "/hugo/admin") { - return ch.Admin().ServeHTTP(w, r) - } - if strings.HasPrefix(r.URL.Path, "/hugo/author") { - return ch.AuthorHome().ServeHTTP(w, r) - } - if strings.HasPrefix(r.URL.Path, "/hugo/edit/") { - return ch.Edit(c).ServeHTTP(w, r) - } - if strings.HasPrefix(r.URL.Path, "/hugo/draft/") { - return ch.serveDraft(w, r) - } - if strings.HasPrefix(r.URL.Path, "/hugo/media") { - return ch.serveMediaPage(w, r) - } - if strings.HasPrefix(r.URL.Path, "/hugo/upload") { - return ch.uploadMedia(w, r) - } - if strings.HasPrefix(r.URL.Path, "/media/") { - return ch.serveMedia(w, r) - } - - http.NotFound(w, r) - return 404, nil -} - -func (ch CaddyHugo) Middleware(c *caddy.Controller) httpserver.Middleware { - return func(next httpserver.Handler) httpserver.Handler { - return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { - return ch.ServeHTTPWithNext(next, c, w, r) - }) - } -} - -func (ch CaddyHugo) Auth(r *http.Request) bool { - return true -} - -func (ch CaddyHugo) Match(r *http.Request) bool { - if strings.HasPrefix(r.URL.Path, "/media/") { - return true - } - - if r.URL.Path == "/hugo" { - return true - } - - return strings.HasPrefix(r.URL.Path, "/hugo/") -} - func (ch CaddyHugo) BasePath() string { return "/hugo" } -func (ch CaddyHugo) Admin() httpserver.Handler { - return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { - err := ch.adminTmpl.Execute(w, ch.TmplData(r, nil)) - if err != nil { - fmt.Println(err) - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil - - }) -} - -func (ch CaddyHugo) AuthorHome() httpserver.Handler { - return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { - td := ch.TmplData(r, nil) - err := ch.authorTmpl.Execute(w, td) - if err != nil { - fmt.Println(err) - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil - }) -} - -func (ch *CaddyHugo) Edit(c *caddy.Controller) httpserver.Handler { - return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { - if r.URL.Path == "/hugo/edit/new" { - return ch.NewContent(w, r) - } - - if r.Header.Get("Upgrade") == "websocket" { - return ch.DeltaWebsocket(w, r) - } - - doc, err := ch.doc(r) - if err != nil { - fmt.Println(err) - return http.StatusNotFound, err - } - - err = ch.editTmpl.Execute(w, ch.TmplData(r, doc)) - if err != nil { - - fmt.Println(err) - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil - }) -} - -func (ch *CaddyHugo) serveDraft(w http.ResponseWriter, r *http.Request) (int, error) { - pathSegments := strings.SplitN(r.URL.Path, "/", 5) - if len(pathSegments) < 4 { - - return http.StatusNotFound, nil - } - - encoded := pathSegments[3] - - nameBytes, err := base64.RawURLEncoding.DecodeString(encoded) - if err != nil { - return http.StatusNotFound, err - } - - name := string(nameBytes) - - ch.mtx.Lock() - defer ch.mtx.Unlock() - - docref, ok := ch.docs[ch.docname(name)] - if !ok { - return http.StatusNotFound, fmt.Errorf("draft not found") - } - - r.URL.Path = strings.ToLower(r.URL.Path) - - prefix := "/hugo/draft/" + encoded - r.URL.Path = r.URL.Path[len(prefix):] - - page := ch.HugoSites.GetContentPage(name) - if page == nil { - fmt.Fprintf(w, "can't find %q to display a draft", name) - return 404, nil - } - - r.URL.Path = page.RelPermalink() - http.FileServer(http.Dir(docref.tmpdir)).ServeHTTP(w, r) - - return 200, nil -} - func (ch *CaddyHugo) docname(orig string) string { return strings.ToLower(orig) } -func (ch *CaddyHugo) doc(r *http.Request) (*docref, error) { - ch.mtx.Lock() - defer ch.mtx.Unlock() - - name := r.URL.Path[len("/hugo/edit/"):] - name = filepath.Join(ch.Site.Root, name) - name = strings.ToLower(name) - - _, ok := ch.docs[ch.docname(name)] - if !ok { - fmt.Println("opening", name) - contents, err := ioutil.ReadFile(name) - if err != nil { - return nil, err - } - - draftPrefix := fmt.Sprintf("draft-%s", base64.RawURLEncoding.EncodeToString([]byte(name))) - tmpdir := path.Join(os.TempDir(), draftPrefix) - - ref := &docref{ - name: name, - doc: acedoc.NewString(string(contents)), - tmpdir: tmpdir, - } - - err = ref.doc.LogToFile(path.Join(ch.Site.Root, "logs", r.URL.Path[len("/hugo/edit/"):])) - if err != nil { - fmt.Println(err) - return nil, err - } - - ch.docs[ch.docname(name)] = ref - - hugoCmd := exec.Command("hugo", "--watch", "-D", "-d", ref.tmpdir) - hugoCmd.Dir = ch.Site.Root - err = hugoCmd.Start() - if err != nil { - return nil, fmt.Errorf("error starting hugo: %v", err) - } - - go func() { - ticker := time.NewTicker(WebsocketFileTicker) - idleTicks := 0 - - defer func() { - err := hugoCmd.Process.Signal(os.Interrupt) - if err != nil { - fmt.Println("error signaling to hugo:", err) - } - err = hugoCmd.Wait() - if err != nil { - fmt.Println("error waiting for hugo:", err) - } - }() - - for { - <-ticker.C - ch.mtx.Lock() - - err := ioutil.WriteFile(name, []byte(ref.doc.Contents()), 0644) - if err != nil { - fmt.Println("error saving document contents:", err) - } - - if ref.clients == 0 { - idleTicks++ - idleTime := time.Duration(idleTicks) * WebsocketFileTicker - if idleTime >= IdleWebsocketTimeout { - err := ch.Publish() - fmt.Printf("idle for %v, quitting\n", idleTime) - if err != nil { - fmt.Printf(", error publishing: %v\n", err) - } - - ref.doc.Close() - os.RemoveAll(tmpdir) - delete(ch.docs, ch.docname(name)) - ch.mtx.Unlock() - return - } - } else { - idleTicks = 0 - } - ch.mtx.Unlock() - } - }() - } - - return ch.docs[ch.docname(name)], nil -} - func (ch *CaddyHugo) Publish() error { cmd := exec.Command("hugo") - cmd.Dir = ch.Site.Root + cmd.Dir = ch.Dir _, err := cmd.CombinedOutput() if err != nil { return err @@ -439,108 +76,6 @@ func (ch *CaddyHugo) Publish() error { return nil } -func (ch *CaddyHugo) DeltaWebsocket(w http.ResponseWriter, r *http.Request) (int, error) { - var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - } - - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - fmt.Println(err) - return http.StatusBadRequest, err - } - - doc, err := ch.doc(r) - if err != nil { - fmt.Println(err) - return http.StatusBadRequest, err - } - - const idlePing = 15 * time.Second - const idlePingShort = 1 * time.Millisecond - var timer *time.Timer - - timer = time.AfterFunc(idlePing, func() { - conn.WriteJSON(Message{ - Deltas: []acedoc.Delta{}, - LTime: ch.LTime(), - }) - timer.Reset(idlePing) - }) - - client := doc.doc.Client(acedoc.DeltaHandlerFunc(func(ds []acedoc.Delta) error { - timer.Reset(idlePing) - err := conn.WriteJSON(Message{ - Deltas: ds, - LTime: ch.LTime(), - }) - return err - })) - - ch.mtx.Lock() - doc.clients++ - ch.mtx.Unlock() - - defer func() { - ch.mtx.Lock() - client.Close() - doc.clients-- - ch.mtx.Unlock() - }() - - for { - var message Message - err := conn.ReadJSON(&message) - if err != nil { - return http.StatusBadRequest, err - } - - ch.ObserveLTime(message.LTime) - timer.Reset(idlePingShort) - - err = client.PushDeltas(message.Deltas...) - if err != nil { - return http.StatusBadRequest, err - } - - } -} - -type Message struct { - Deltas []acedoc.Delta `json:"deltas"` - LTime uint64 `json:"ltime"` -} - -func (ch CaddyHugo) NewContent(w http.ResponseWriter, r *http.Request) (int, error) { - name := r.FormValue("name") - ctype := r.FormValue("type") - - if filepath.Ext(name) != ".md" { - name += ".md" - } - - filename := path.Join(ctype, strings.ToLower(name)) - if ctype == "default" { - filename = strings.ToLower(name) - } - - _, err := os.Stat(path.Join(ch.Site.Root, "content", filename)) - if os.IsNotExist(err) { - cmd := exec.Command("hugo", "new", filename) - cmd.Dir = ch.Site.Root - out, err := cmd.CombinedOutput() - if err != nil { - fmt.Println("error running hugo:\n", string(out)) - return http.StatusInternalServerError, err - } - } - - // serve redirect - http.Redirect(w, r, filepath.Join("/hugo/edit/", "content", filename), http.StatusFound) - return http.StatusFound, nil -} - func (ch CaddyHugo) TmplData(r *http.Request, docref *docref) interface{} { var doc *acedoc.Document if docref != nil { diff --git a/client.go b/client.go new file mode 100644 index 0000000..2d88240 --- /dev/null +++ b/client.go @@ -0,0 +1,114 @@ +package caddyhugo + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "time" + + "git.stephensearles.com/stephen/acedoc" +) + +type docref struct { + clients uint + name string + doc *acedoc.Document + tmpdir string +} + +func (ch *CaddyHugo) doc(r *http.Request) (*docref, error) { + ch.mtx.Lock() + defer ch.mtx.Unlock() + + name := r.URL.Path[len("/hugo/edit/"):] + name = filepath.Join(ch.Dir, name) + name = strings.ToLower(name) + + _, ok := ch.docs[ch.docname(name)] + if !ok { + fmt.Println("opening", name) + contents, err := ioutil.ReadFile(name) + if err != nil { + return nil, err + } + + draftPrefix := fmt.Sprintf("draft-%s", base64.RawURLEncoding.EncodeToString([]byte(name))) + tmpdir := path.Join(os.TempDir(), draftPrefix) + + ref := &docref{ + name: name, + doc: acedoc.NewString(string(contents)), + tmpdir: tmpdir, + } + + err = ref.doc.LogToFile(path.Join(ch.Dir, "logs", r.URL.Path[len("/hugo/edit/"):])) + if err != nil { + fmt.Println(err) + return nil, err + } + + ch.docs[ch.docname(name)] = ref + + hugoCmd := exec.Command("hugo", "--watch", "-D", "-d", ref.tmpdir) + hugoCmd.Dir = ch.Dir + err = hugoCmd.Start() + if err != nil { + return nil, fmt.Errorf("error starting hugo: %v", err) + } + + go func() { + ticker := time.NewTicker(WebsocketFileTicker) + idleTicks := 0 + + defer func() { + err := hugoCmd.Process.Signal(os.Interrupt) + if err != nil { + fmt.Println("error signaling to hugo:", err) + } + err = hugoCmd.Wait() + if err != nil { + fmt.Println("error waiting for hugo:", err) + } + }() + + for { + <-ticker.C + ch.mtx.Lock() + + err := ioutil.WriteFile(name, []byte(ref.doc.Contents()), 0644) + if err != nil { + fmt.Println("error saving document contents:", err) + } + + if ref.clients == 0 { + idleTicks++ + idleTime := time.Duration(idleTicks) * WebsocketFileTicker + if idleTime >= IdleWebsocketTimeout { + err := ch.Publish() + fmt.Printf("idle for %v, quitting\n", idleTime) + if err != nil { + fmt.Printf(", error publishing: %v\n", err) + } + + ref.doc.Close() + os.RemoveAll(tmpdir) + delete(ch.docs, ch.docname(name)) + ch.mtx.Unlock() + return + } + } else { + idleTicks = 0 + } + ch.mtx.Unlock() + } + }() + } + + return ch.docs[ch.docname(name)], nil +} diff --git a/content.go b/content.go index b7ceba7..f93bc28 100644 --- a/content.go +++ b/content.go @@ -3,9 +3,11 @@ package caddyhugo import ( "fmt" "os" + "os/exec" "path" "path/filepath" "sort" + "strings" "time" "github.com/gohugoio/hugo/hugolib" @@ -80,3 +82,31 @@ func GetContent(siteRoot string, sites *hugolib.HugoSites) ([]Content, error) { return files, nil } + +func (ch CaddyHugo) NewContent(name, ctype string) (string, error) { + if filepath.Ext(name) != ".md" { + name += ".md" + } + + if ctype == "" { + ctype = "default" + } + + filename := path.Join(ctype, strings.ToLower(name)) + if ctype == "default" { + filename = strings.ToLower(name) + } + + _, err := os.Stat(path.Join(ch.Dir, "content", filename)) + if os.IsNotExist(err) { + cmd := exec.Command("hugo", "new", filename) + cmd.Dir = ch.Dir + out, err := cmd.CombinedOutput() + if err != nil { + return filename, fmt.Errorf("error running 'hugo new': %v; %v", err, string(out)) + return filename, err + } + } + + return filename, nil +} diff --git a/deltas.go b/deltas.go new file mode 100644 index 0000000..f1cda61 --- /dev/null +++ b/deltas.go @@ -0,0 +1,106 @@ +package caddyhugo + +import ( + "fmt" + "net/http" + "time" + + "git.stephensearles.com/stephen/acedoc" + "github.com/gorilla/websocket" +) + +const ( + IdleWebsocketTimeout = 10 * time.Minute + WebsocketFileTicker = 1 * time.Second +) + +func (ch *CaddyHugo) ObserveLTime(ltime uint64) uint64 { + ch.mtx.Lock() + + if ch.ltime < ltime { + ch.ltime = ltime + } + + ch.mtx.Unlock() + return ch.LTime() +} + +func (ch *CaddyHugo) LTime() uint64 { + ch.mtx.Lock() + defer ch.mtx.Unlock() + ch.ltime++ + return ch.ltime +} + +func (ch *CaddyHugo) DeltaWebsocket(w http.ResponseWriter, r *http.Request) (int, error) { + var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + fmt.Println(err) + return http.StatusBadRequest, err + } + + doc, err := ch.doc(r) + if err != nil { + fmt.Println(err) + return http.StatusBadRequest, err + } + + const idlePing = 15 * time.Second + const idlePingShort = 1 * time.Millisecond + var timer *time.Timer + + timer = time.AfterFunc(idlePing, func() { + conn.WriteJSON(Message{ + Deltas: []acedoc.Delta{}, + LTime: ch.LTime(), + }) + timer.Reset(idlePing) + }) + + client := doc.doc.Client(acedoc.DeltaHandlerFunc(func(ds []acedoc.Delta) error { + timer.Reset(idlePing) + err := conn.WriteJSON(Message{ + Deltas: ds, + LTime: ch.LTime(), + }) + return err + })) + + ch.mtx.Lock() + doc.clients++ + ch.mtx.Unlock() + + defer func() { + ch.mtx.Lock() + client.Close() + doc.clients-- + ch.mtx.Unlock() + }() + + for { + var message Message + err := conn.ReadJSON(&message) + if err != nil { + return http.StatusBadRequest, err + } + + ch.ObserveLTime(message.LTime) + timer.Reset(idlePingShort) + + err = client.PushDeltas(message.Deltas...) + if err != nil { + return http.StatusBadRequest, err + } + + } +} + +type Message struct { + Deltas []acedoc.Delta `json:"deltas"` + LTime uint64 `json:"ltime"` +} diff --git a/doc_test.go b/doc_test.go new file mode 100644 index 0000000..9f9e81e --- /dev/null +++ b/doc_test.go @@ -0,0 +1,61 @@ +package caddyhugo + +import ( + "io/ioutil" + "os" + "os/exec" + "testing" +) + +type World struct { + CH CaddyHugo + BlogFolder string +} + +func (w World) Clean() { + if w.BlogFolder != "" { + os.RemoveAll(w.BlogFolder) + } +} + +func NewWorld(t *testing.T) World { + dir, err := ioutil.TempDir("", "caddy-hugo2-test-") + if err != nil { + t.Fatalf("error initializing test environment: %v", err) + } + + w := World{BlogFolder: dir} + + cmd := exec.Command("hugo", "new", "site", dir) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("error initializing test site: %v\n\n%v", err, string(out)) + } + + w.CH.Setup(dir) + + return w +} + +func TestDoc(t *testing.T) { + w := NewWorld(t) + defer w.Clean() + + c, err := GetContent(w.BlogFolder, w.CH.HugoSites) + if err != nil { + t.Fatalf("couldn't get content from a blank test environment: %v", err) + } + if len(c) != 0 { + t.Fatalf("expected a blank test environment, but saw %d pages", len(c)) + } + + w.CH.NewContent("test1", "") + c, err = GetContent(w.BlogFolder, w.CH.HugoSites) + if err != nil { + t.Fatalf("couldn't get content from the test environment: %v", err) + } + if len(c) != 1 { + t.Fatalf("expected 1 page, but saw %d pages", len(c)) + } +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..21e07db --- /dev/null +++ b/http.go @@ -0,0 +1,202 @@ +package caddyhugo + +import ( + "encoding/base64" + "errors" + "fmt" + "net/http" + "path" + "path/filepath" + "strings" + + "github.com/g3n/engine/gui/assets" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func (ch *CaddyHugo) ServeHTTPWithNext(next httpserver.Handler, c *caddy.Controller, w http.ResponseWriter, r *http.Request) (int, error) { + if !ch.Match(r) { + p := path.Join(ch.Dir, "public", r.URL.Path) + http.ServeFile(w, r, p) + return 200, nil + } + + if !ch.Auth(r) { + return http.StatusUnauthorized, errors.New("not authorized") + } + + if strings.HasPrefix(r.URL.Path, "/hugo/publish") { + err := ch.Publish() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return http.StatusInternalServerError, nil + } + + http.Redirect(w, r, "/", http.StatusFound) + return http.StatusFound, nil + } + if strings.HasPrefix(r.URL.Path, "/hugo/simplemde.css") { + w.Write(assets.MustAsset("simplemde/dist/simplemde.min.css")) + return http.StatusOK, nil + } + if strings.HasPrefix(r.URL.Path, "/hugo/simplemde.js") { + w.Write(assets.MustAsset("simplemde/debug/simplemde.js")) + return http.StatusOK, nil + } + if strings.HasPrefix(r.URL.Path, "/hugo/admin") { + return ch.Admin().ServeHTTP(w, r) + } + if strings.HasPrefix(r.URL.Path, "/hugo/author") { + return ch.AuthorHome().ServeHTTP(w, r) + } + if strings.HasPrefix(r.URL.Path, "/hugo/edit/") { + return ch.Edit(c).ServeHTTP(w, r) + } + if strings.HasPrefix(r.URL.Path, "/hugo/draft/") { + return ch.serveDraft(w, r) + } + if strings.HasPrefix(r.URL.Path, "/hugo/media") { + return ch.serveMediaPage(w, r) + } + if strings.HasPrefix(r.URL.Path, "/hugo/upload") { + return ch.uploadMedia(w, r) + } + if strings.HasPrefix(r.URL.Path, "/media/") { + return ch.serveMedia(w, r) + } + + http.NotFound(w, r) + return 404, nil +} + +func (ch CaddyHugo) ServeNewContent(w http.ResponseWriter, r *http.Request) (int, error) { + name := r.FormValue("name") + ctype := r.FormValue("type") + + filename, err := ch.NewContent(name, ctype) + if err != nil { + fmt.Println("error creating new content:", err) + return http.StatusInternalServerError, err + } + + // serve redirect + http.Redirect(w, r, filepath.Join("/hugo/edit/", "content", filename), http.StatusFound) + return http.StatusFound, nil +} +func (ch CaddyHugo) Middleware(c *caddy.Controller) httpserver.Middleware { + return func(next httpserver.Handler) httpserver.Handler { + return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + return ch.ServeHTTPWithNext(next, c, w, r) + }) + } +} + +func (ch CaddyHugo) Auth(r *http.Request) bool { + return true +} + +func (ch CaddyHugo) Match(r *http.Request) bool { + if strings.HasPrefix(r.URL.Path, "/media/") { + return true + } + + if r.URL.Path == "/hugo" { + return true + } + + return strings.HasPrefix(r.URL.Path, "/hugo/") +} + +func (ch CaddyHugo) Admin() httpserver.Handler { + return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + err := ch.adminTmpl.Execute(w, ch.TmplData(r, nil)) + if err != nil { + fmt.Println(err) + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil + + }) +} + +func (ch CaddyHugo) AuthorHome() httpserver.Handler { + return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + td := ch.TmplData(r, nil) + err := ch.authorTmpl.Execute(w, td) + if err != nil { + fmt.Println(err) + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil + }) +} + +func (ch *CaddyHugo) Edit(c *caddy.Controller) httpserver.Handler { + return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + if r.URL.Path == "/hugo/edit/new" { + return ch.ServeNewContent(w, r) + } + + if r.Header.Get("Upgrade") == "websocket" { + return ch.DeltaWebsocket(w, r) + } + + doc, err := ch.doc(r) + if err != nil { + fmt.Println(err) + return http.StatusNotFound, err + } + + err = ch.editTmpl.Execute(w, ch.TmplData(r, doc)) + if err != nil { + + fmt.Println(err) + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil + }) +} + +func (ch *CaddyHugo) serveDraft(w http.ResponseWriter, r *http.Request) (int, error) { + pathSegments := strings.SplitN(r.URL.Path, "/", 5) + if len(pathSegments) < 4 { + + return http.StatusNotFound, nil + } + + encoded := pathSegments[3] + + nameBytes, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return http.StatusNotFound, err + } + + name := string(nameBytes) + + ch.mtx.Lock() + defer ch.mtx.Unlock() + + docref, ok := ch.docs[ch.docname(name)] + if !ok { + return http.StatusNotFound, fmt.Errorf("draft not found") + } + + r.URL.Path = strings.ToLower(r.URL.Path) + + prefix := "/hugo/draft/" + encoded + r.URL.Path = r.URL.Path[len(prefix):] + + page := ch.HugoSites.GetContentPage(name) + if page == nil { + fmt.Fprintf(w, "can't find %q to display a draft", name) + return 404, nil + } + + r.URL.Path = page.RelPermalink() + http.FileServer(http.Dir(docref.tmpdir)).ServeHTTP(w, r) + + return 200, nil +} diff --git a/setup.go b/setup.go new file mode 100644 index 0000000..ec5e4e8 --- /dev/null +++ b/setup.go @@ -0,0 +1,78 @@ +package caddyhugo + +import ( + "fmt" + "html/template" + "io/ioutil" + "os" + "path" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/hugolib" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func (ch *CaddyHugo) SetupCaddy(c *caddy.Controller) error { + ch.Site = httpserver.GetConfig(c) + ch.Site.AddMiddleware(ch.Middleware(c)) + return ch.Setup(ch.Dir) +} + +func (ch *CaddyHugo) Setup(dir string) error { + var err error + + ch.Dir = dir + ch.docs = make(map[string]*docref) + + ch.HugoCfg = &deps.DepsCfg{} + + ch.HugoCfg.Cfg, err = hugolib.LoadConfig(hugofs.Os, dir, "") + if err != nil { + return fmt.Errorf("error loading hugo config: %v", err) + } + + ch.HugoCfg.Cfg.Set("workingdir", dir) + ch.HugoSites, err = hugolib.NewHugoSites(*ch.HugoCfg) + if err != nil { + return fmt.Errorf("error intializing hugo: %v", err) + } + + err = ch.Build() + if err != nil { + return fmt.Errorf("error building initial hugo: %v", err) + } + + ch.authorTmpl, err = template.New("").Parse(AuthorPage) + if err != nil { + return fmt.Errorf("author template invalid: %v", err) + } + + ch.adminTmpl, err = template.New("").Parse(AdminPage) + if err != nil { + return fmt.Errorf("admin template invalid: %v", err) + } + + ch.editTmpl, err = template.New("").Parse(EditPage) + if err != nil { + return fmt.Errorf("edit template invalid: %v", err) + } + + thumbDir, err := ioutil.TempDir("", "thumbs") + if err != nil { + return fmt.Errorf("couldn't initialize media: %v", err) + } + + ch.Media = &MediaSource{ + StorageDir: path.Join(dir, "media"), + ThumbDir: thumbDir, + } + + err = os.MkdirAll(ch.Media.StorageDir, 0755) + if err != nil { + return fmt.Errorf("couldn't initialize media: %v", err) + } + + return ch.Publish() +}