poc of functions
This commit is contained in:
254
cmd/actions.go
254
cmd/actions.go
@@ -1,42 +1,276 @@
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sotigr/slic3r-api/internal/cache"
|
||||
"github.com/sotigr/slic3r-api/internal/slicer"
|
||||
"github.com/sotigr/slic3r-api/internal/thumbnail"
|
||||
)
|
||||
|
||||
var allowedModelExtensions = map[string]bool{
|
||||
".stl": true,
|
||||
".3mf": true,
|
||||
".obj": true,
|
||||
".amf": true,
|
||||
}
|
||||
|
||||
func getEnvOrDefault(key, defaultVal string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
func actions(mux *http.ServeMux) {
|
||||
slicerSvc := slicer.NewSlicerService(
|
||||
getEnvOrDefault("FFF_CONFIG_PATH", "configs/p1s-cfg.ini"),
|
||||
getEnvOrDefault("SLA_CONFIG_PATH", "configs/gktwo-sla-cfg.ini"),
|
||||
)
|
||||
thumbnailSvc := thumbnail.NewThumbnailService()
|
||||
|
||||
var c cache.Cache
|
||||
if url := getEnvOrDefault("VALKEY_URL", ""); url != "" {
|
||||
vc, err := cache.NewValkeyCache(url)
|
||||
if err != nil {
|
||||
log.Printf("cache: failed to connect to Valkey at %s: %v — running without cache", url, err)
|
||||
c = cache.NewNoopCache()
|
||||
} else {
|
||||
log.Printf("cache: connected to Valkey at %s", url)
|
||||
c = vc
|
||||
}
|
||||
} else {
|
||||
log.Print("cache: VALKEY_URL not set — running without cache")
|
||||
c = cache.NewNoopCache()
|
||||
}
|
||||
|
||||
cacheTTL := 24 * time.Hour
|
||||
if s := getEnvOrDefault("CACHE_TTL_SECONDS", ""); s != "" {
|
||||
if v, err := strconv.ParseInt(s, 10, 64); err == nil && v > 0 {
|
||||
cacheTTL = time.Duration(v) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
|
||||
})
|
||||
|
||||
mux.HandleFunc("/analyze-fff", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
modelPath, cleanup, err := saveUploadedFile(r, "file")
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to read file: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
opts, err := parseAnalyzeOptions(r)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid options: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileHash, err := cache.HashFile(modelPath)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to hash file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
key := cache.AnalysisKey("fff", fileHash, opts.Scale)
|
||||
|
||||
if cached, ok := c.Get(r.Context(), key); ok {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(cached)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := slicerSvc.AnalyzeFFF(modelPath, opts)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("analysis failed: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(result)
|
||||
c.Set(r.Context(), key, payload, cacheTTL)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(payload)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/analyze-sla", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
modelPath, cleanup, err := saveUploadedFile(r, "file")
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to read file: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
opts, err := parseAnalyzeOptions(r)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid options: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileHash, err := cache.HashFile(modelPath)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to hash file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
key := cache.AnalysisKey("sla", fileHash, opts.Scale)
|
||||
|
||||
if cached, ok := c.Get(r.Context(), key); ok {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(cached)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := slicerSvc.AnalyzeSLA(modelPath, opts)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("analysis failed: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(result)
|
||||
c.Set(r.Context(), key, payload, cacheTTL)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(payload)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/thumbnail", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
modelPath, cleanup, err := saveUploadedFile(r, "file")
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to read file: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
fileHash, err := cache.HashFile(modelPath)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to hash file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
key := cache.ThumbnailKey(fileHash)
|
||||
|
||||
if cached, ok := c.Get(r.Context(), key); ok {
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(cached)
|
||||
return
|
||||
}
|
||||
|
||||
outputPath := filepath.Join(filepath.Dir(modelPath), "thumbnail.webp")
|
||||
|
||||
if err := thumbnailSvc.Generate(r.Context(), modelPath, outputPath); err != nil {
|
||||
http.Error(w, fmt.Sprintf("thumbnail generation failed: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
pngData, err := os.ReadFile(outputPath)
|
||||
os.Remove(outputPath)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to read generated thumbnail", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(r.Context(), key, pngData, cacheTTL)
|
||||
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(pngData)
|
||||
})
|
||||
}
|
||||
|
||||
// saveUploadedFile reads the named file field from a multipart request and saves it to a
|
||||
// temporary directory. Returns the saved path and a cleanup function to remove the directory.
|
||||
func saveUploadedFile(r *http.Request, field string) (string, func(), error) {
|
||||
const maxUploadSize = 512 << 20 // 64 MB
|
||||
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
|
||||
return "", nil, fmt.Errorf("parse multipart form: %w", err)
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile(field)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("get form field %q: %w", field, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
origName := filepath.Base(header.Filename)
|
||||
ext := strings.ToLower(filepath.Ext(origName))
|
||||
if !allowedModelExtensions[ext] {
|
||||
return "", nil, fmt.Errorf("unsupported file format %q, allowed: .stl .3mf .obj .amf", ext)
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "upload-*")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("create temp dir: %w", err)
|
||||
}
|
||||
cleanup := func() { os.RemoveAll(tmpDir) }
|
||||
|
||||
dst := filepath.Join(tmpDir, uuid.New().String()+ext)
|
||||
f, err := os.Create(dst)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, file); err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("write uploaded file: %w", err)
|
||||
}
|
||||
|
||||
return dst, cleanup, nil
|
||||
}
|
||||
|
||||
// parseAnalyzeOptions reads optional analysis parameters from the multipart form.
|
||||
// Supported fields: scale (float, e.g. "1.5" = 150% size).
|
||||
func parseAnalyzeOptions(r *http.Request) (slicer.AnalyzeOptions, error) {
|
||||
var opts slicer.AnalyzeOptions
|
||||
if s := r.FormValue("scale"); s != "" {
|
||||
scale, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil || scale <= 0 {
|
||||
return opts, fmt.Errorf("scale must be a positive number of millimeters, got %q", s)
|
||||
}
|
||||
opts.Scale = scale
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var memLimitMb int64 = 512
|
||||
debug.SetMemoryLimit(memLimitMb * 1024 * 1024)
|
||||
// Your code here
|
||||
mux := http.NewServeMux()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user