Initial commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user