diff --git a/caddyhugo.go b/caddyhugo.go index ef80f04..3b5caff 100644 --- a/caddyhugo.go +++ b/caddyhugo.go @@ -52,6 +52,8 @@ type CaddyHugo struct { ServerType string Site *httpserver.SiteConfig + Media *MediaSource + docs map[string]*docref mtx sync.Mutex @@ -100,6 +102,21 @@ func (ch *CaddyHugo) Setup(c *caddy.Controller) error { 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)) @@ -148,6 +165,12 @@ func (ch *CaddyHugo) ServeHTTPWithNext(next httpserver.Handler, c *caddy.Control 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, "/media/") { + return ch.serveMedia(w, r) + } http.NotFound(w, r) return 404, nil @@ -166,6 +189,10 @@ func (ch CaddyHugo) Auth(r *http.Request) bool { } func (ch CaddyHugo) Match(r *http.Request) bool { + if strings.HasPrefix(r.URL.Path, "/media/") { + return true + } + if r.URL.Path == "/hugo" { return true } diff --git a/media.go b/media.go new file mode 100644 index 0000000..f34bf20 --- /dev/null +++ b/media.go @@ -0,0 +1,214 @@ +package caddyhugo + +import ( + "fmt" + "image" + "image/jpeg" + "io" + "net/http" + "os" + "path" + "path/filepath" + "strconv" + "strings" + + "github.com/nfnt/resize" +) + +type MediaSource struct { + StorageDir string + ThumbDir string +} + +func (ms *MediaSource) LocationOrig(m Media) string { + return path.Join(ms.StorageDir, m.Name) +} + +type Media struct { + Type string + Name string +} + +func (ms *MediaSource) ThumbMax(m Media, maxDim int) (string, error) { + f, err := os.Open(ms.LocationOrig(m)) + if err != nil { + return "", err + } + defer f.Close() + + img, _, err := image.Decode(f) + if err != nil { + return "", err + } + + rect := img.Bounds() + width := rect.Dx() + height := rect.Dy() + + if width > height { + height = height * maxDim / width + width = maxDim + } else { + width = width * maxDim / height + height = maxDim + } + + return ms.ThumbImage(img, m, width, height) +} + +func (ms *MediaSource) ByName(name string) *Media { + return &Media{ + Type: "image", + Name: name, + } +} + +func (ms *MediaSource) Thumb(m Media, width, height int) (string, error) { + f, err := os.Open(ms.LocationOrig(m)) + if err != nil { + return "", err + } + defer f.Close() + + img, _, err := image.Decode(f) + if err != nil { + return "", err + } + + return ms.ThumbImage(img, m, width, height) +} + +func (ms *MediaSource) ThumbImage(img image.Image, m Media, width, height int) (string, error) { + + thumbSlug := filepath.Join(m.Name, fmt.Sprintf("%d/%d.jpg", width, height)) + thumbLoc := filepath.Join(ms.ThumbDir, thumbSlug) + + os.MkdirAll(path.Dir(thumbLoc), 0755) + fthumb, err := os.OpenFile(thumbLoc, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0655) + if err != nil { + return "", err + } + + img = resize.Resize(uint(width), uint(height), img, resize.Bilinear) + + err = jpeg.Encode(fthumb, img, nil) + if err != nil { + return "", err + } + + err = fthumb.Close() + if err != nil { + return "", err + } + + return path.Join("/media", thumbSlug), nil +} + +func (ms *MediaSource) Walk() ([]*Media, error) { + var media []*Media + + err := filepath.Walk(ms.StorageDir, func(name string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.IsDir() { + return nil + } + media = append(media, ms.ByName(path.Base(name))) + return nil + }) + + return media, err +} + +func (ch *CaddyHugo) serveMediaPage(w http.ResponseWriter, r *http.Request) (int, error) { + if ch.Media == nil { + http.NotFound(w, r) + return 404, nil + } + + io.WriteString(w, ` + + + `) + if ch.Media != nil { + media, err := ch.Media.Walk() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return 500, nil + } + + for _, m := range media { + src, err := ch.Media.ThumbMax(*m, 100) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return 500, nil + } + fmt.Fprintf(w, "\n", src) + } + } + io.WriteString(w, ``) + return 200, nil +} + +func (ch *CaddyHugo) serveMedia(w http.ResponseWriter, r *http.Request) (int, error) { + if ch.Media == nil { + http.NotFound(w, r) + return 404, nil + } + + segs := strings.Split(r.URL.Path, "/") + name := segs[2] + file := "" + var err error + m := ch.Media.ByName(name) + + switch len(segs) { + case 3: + file = name + case 4: + var max int + max, err = strconv.Atoi(removeExtension(segs[3])) + if err != nil { + http.Error(w, fmt.Sprintf("expected /media/filename, /media/filename/maxDim, or /media/filename/width/height..."), http.StatusBadRequest) + return 400, nil + } + + file, err = ch.Media.ThumbMax(*m, max) + case 5: + var width, height int + width, err = strconv.Atoi(segs[3]) + if err != nil { + http.Error(w, fmt.Sprintf("expected /media/filename, /media/filename/maxDim, or /media/filename/width/height.."), http.StatusBadRequest) + return 400, nil + } + height, err = strconv.Atoi(removeExtension(segs[4])) + if err != nil { + http.Error(w, fmt.Sprintf("expected /media/filename, /media/filename/maxDim, or /media/filename/width/height."), http.StatusBadRequest) + return 400, nil + } + file, err = ch.Media.Thumb(*m, width, height) + default: + http.Error(w, fmt.Sprintf("expected /media/filename, /media/filename/maxDim, or /media/filename/width/height"), http.StatusBadRequest) + return 400, nil + } + + if err != nil { + http.Error(w, fmt.Sprintf("unable to load thumb"), http.StatusInternalServerError) + return 500, nil + } + + if file[0] == '/' { + file = file[1:] + } + + file = path.Join(ch.Media.ThumbDir, file[len("media/"):]) + http.ServeFile(w, r, file) + + return 200, nil +} + +func removeExtension(name string) string { + ext := path.Ext(name) + return name[:len(name)-len(ext)] +}