153 lines
4.0 KiB
Go
153 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
type Server struct {
|
|
port string
|
|
contentDir string
|
|
outputDir string
|
|
serve bool
|
|
generator *Generator
|
|
}
|
|
|
|
func NewServer(port, contentDir, outputDir string, serve bool, g *Generator) *Server {
|
|
return &Server{
|
|
port: port,
|
|
contentDir: contentDir,
|
|
outputDir: outputDir,
|
|
serve: serve,
|
|
generator: g,
|
|
}
|
|
}
|
|
|
|
func (s *Server) Listen() error {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /upload", s.handleUploadPage)
|
|
mux.HandleFunc("POST /upload", s.handleUpload)
|
|
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintln(w, "ok")
|
|
})
|
|
|
|
if s.serve {
|
|
// Serve the generated site on the same port — for local development only.
|
|
// In production nginx handles this and the server binds to localhost.
|
|
mux.Handle("/", http.FileServer(http.Dir(s.outputDir)))
|
|
log.Printf("gallery: http://localhost:%s", s.port)
|
|
log.Printf("upload: http://localhost:%s/upload", s.port)
|
|
return http.ListenAndServe(":"+s.port, mux)
|
|
}
|
|
|
|
log.Printf("upload API on 127.0.0.1:%s", s.port)
|
|
return http.ListenAndServe("127.0.0.1:"+s.port, mux)
|
|
}
|
|
|
|
func (s *Server) handleUploadPage(w http.ResponseWriter, r *http.Request) {
|
|
data, err := assets.ReadFile("templates/upload.html")
|
|
if err != nil {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Write(data)
|
|
}
|
|
|
|
// handleUpload accepts multipart/form-data with fields:
|
|
//
|
|
// image — the image file (jpg or png)
|
|
// caption — human-readable caption (optional, defaults to filename)
|
|
// date — YYYY-MM-DD (optional, defaults to today)
|
|
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
file, header, err := r.FormFile("image")
|
|
if err != nil {
|
|
http.Error(w, "missing 'image' field", http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
ext := strings.ToLower(filepath.Ext(header.Filename))
|
|
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
|
|
http.Error(w, "only jpg and png are accepted", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
caption := r.FormValue("caption")
|
|
dateStr := r.FormValue("date")
|
|
if dateStr == "" {
|
|
dateStr = time.Now().Format("2006-01-02")
|
|
}
|
|
if caption == "" {
|
|
caption = strings.TrimSuffix(header.Filename, ext)
|
|
}
|
|
|
|
slug := sanitiseSlug(strings.TrimSuffix(header.Filename, ext))
|
|
if slug == "" {
|
|
slug = fmt.Sprintf("photo-%d", time.Now().UnixMilli())
|
|
}
|
|
imgPath := filepath.Join(s.contentDir, slug+ext)
|
|
tomlPath := filepath.Join(s.contentDir, slug+".toml")
|
|
|
|
out, err := os.Create(imgPath)
|
|
if err != nil {
|
|
log.Printf("create %s: %v", imgPath, err)
|
|
http.Error(w, "could not save image", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer out.Close()
|
|
if _, err := io.Copy(out, file); err != nil {
|
|
http.Error(w, "could not write image", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
tf, err := os.Create(tomlPath)
|
|
if err != nil {
|
|
log.Printf("create %s: %v", tomlPath, err)
|
|
http.Error(w, "could not save metadata", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer tf.Close()
|
|
if err := toml.NewEncoder(tf).Encode(PhotoMeta{Caption: caption, Date: dateStr}); err != nil {
|
|
http.Error(w, "could not write metadata", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Build() is mutex-guarded so calling it here and from the watcher is safe.
|
|
go func() {
|
|
if err := s.generator.Build(); err != nil {
|
|
log.Printf("rebuild after upload: %v", err)
|
|
}
|
|
}()
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
fmt.Fprintf(w, "uploaded %s\n", slug+ext)
|
|
}
|
|
|
|
// sanitiseSlug returns a filesystem- and URL-safe version of s.
|
|
func sanitiseSlug(s string) string {
|
|
var b strings.Builder
|
|
for _, r := range strings.ToLower(s) {
|
|
switch {
|
|
case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-':
|
|
b.WriteRune(r)
|
|
case r == ' ', r == '_':
|
|
b.WriteRune('-')
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|