package main import ( "context" "crypto/rand" "embed" "encoding/json" "flag" "fmt" "io" "io/fs" "net/http" "os" "path" "sync/atomic" "time" "log/slog" "go.balki.me/anyhttp" "go.balki.me/anyhttp/idle" "go.balki.me/collage-maker/collage" ) var ( imagesDir string collageDir string localAssets bool collageNameGen *nameGen imagesDirFs fs.FS listenAddr string // go:embed web/* webFS embed.FS ) func main() { flag.StringVar(&imagesDir, "images-dir", "images", "Sets the images dir") flag.StringVar(&collageDir, "collages-dir", "collages", "Sets the collages dir") flag.BoolVar(&localAssets, "local-assets", false, "Serve local assets for testing") flag.StringVar(&listenAddr, "addr", "127.0.0.1:8767", "Sets the collages dir") flag.Parse() collageNameGen = NewNameGen() imagesDirFs = os.DirFS(imagesDir) imagesURLPath := "images" collagesPath := "collages" addFileServer := func(path, dir string) { httpFileServer := http.FileServer(http.Dir(dir)) http.Handle("/"+path+"/", http.StripPrefix("/"+path, httpFileServer)) } addFileServer(imagesURLPath, imagesDir) addFileServer(collagesPath, collageDir) if localAssets { httpFileServer := http.FileServer(http.Dir("web")) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Cache-Control", "no-cache") httpFileServer.ServeHTTP(w, r) }) } else { fs, err := fs.Sub(webFS, "web") if err != nil { panic(err) } httpFileServer := http.FileServer(http.FS(fs)) http.Handle("/", httpFileServer) } http.HandleFunc("/make-collage", func(w http.ResponseWriter, r *http.Request) { collageReq := collage.Request{} body, err := io.ReadAll(r.Body) if err != nil { slog.Error("failed to read request body", "error", err) w.WriteHeader(http.StatusInternalServerError) return } if err := json.Unmarshal(body, &collageReq); err != nil { slog.Error("failed to unmarshal json request", "error", err) w.WriteHeader(http.StatusInternalServerError) return } collageFile, err := MakeCollage(&collageReq) if err != nil { slog.Error("failed to make collage", "error", err) w.WriteHeader(http.StatusInternalServerError) return } if _, err := w.Write([]byte(collageFile)); err != nil { slog.Error("Failed to write collageFile", "error", err) } }) addrType, server, err := anyhttp.ListenAndServeHTTP(listenAddr, idle.WrapHandler(nil)) if err != nil { slog.Error("anyhttp ListenAndServeHTTP failed", "error", err) } if addrType == anyhttp.SystemdFD { if err := idle.Wait(1 * time.Minute); err != nil { slog.Error("Failed to wait for idler", "error", err) } ctx, _ := context.WithTimeout(context.Background(), 1*time.Minute) // Don't want any stuck connections if err := server.Shutdown(ctx); err != nil { slog.Error("http server Shutdown failed", "error", err) } } else { select {} } } type nameGen struct { prefix string counter atomic.Uint64 } func NewNameGen() *nameGen { currentTime := time.Now().Unix() randBytes := make([]byte, 8) _, err := rand.Read(randBytes) if err != nil { panic(err) } alpha := []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ") uniqRunID := "" for _, b := range randBytes { uniqRunID += string(alpha[int(b)%len(alpha)]) } return &nameGen{ prefix: fmt.Sprintf("%d-%s", currentTime, uniqRunID), counter: atomic.Uint64{}, } } func (n *nameGen) Next() string { return fmt.Sprintf("%s-%d", n.prefix, n.counter.Add(1)) } func MakeCollage(req *collage.Request) (string, error) { collageFile := fmt.Sprintf("collage-%s.jpg", collageNameGen.Next()) out, err := os.Create(path.Join(collageDir, collageFile)) if err != nil { return "", fmt.Errorf("failed to create collage output file, err: %w", err) } if err := collage.Make(req, imagesDirFs, out); err != nil { return "", fmt.Errorf("failed to make collage, err: %w", err) } return collageFile, nil } // curl -D - --json @req.json http://localhost:8767/make-collage