package server import ( "context" "encoding/json" "errors" "log" "net/http" "net/url" "os" "path/filepath" "strings" "time" "git.meatbag.se/varl/subsyt/internal/config" ) const defaultListen = "127.0.0.1:4416" type Server struct { cfg config.Http_api queue *Queue defaultOutDir string httpServer *http.Server shutdownSignal chan struct{} authToken string } func NewServer(cfg config.Http_api, defaultOutDir string, queue *Queue) *Server { srv := &Server{ cfg: cfg, queue: queue, defaultOutDir: defaultOutDir, shutdownSignal: make(chan struct{}), } srv.authToken = strings.TrimSpace(cfg.Auth_token) if srv.authToken == "" && cfg.Auth_token_file != "" { if token, err := os.ReadFile(cfg.Auth_token_file); err == nil { srv.authToken = strings.TrimSpace(string(token)) } else { log.Printf("failed to read auth token file %s: %v", cfg.Auth_token_file, err) } } return srv } func (s *Server) Start(ctx context.Context) error { mux := http.NewServeMux() mux.HandleFunc("/v1/videos", s.handleVideos) mux.HandleFunc("/status", s.handleStatus) addr := s.cfg.Listen if addr == "" { addr = defaultListen } s.httpServer = &http.Server{ Addr: addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } go func() { select { case <-ctx.Done(): case <-s.shutdownSignal: } shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if s.httpServer != nil { if err := s.httpServer.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Printf("http shutdown error: %v", err) } } }() err := s.httpServer.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil } func (s *Server) Stop() { close(s.shutdownSignal) } func (s *Server) handleVideos(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if !s.authorize(r.Header.Get("Authorization")) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var payload VideoRequest dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(&payload); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } if payload.URL == "" { http.Error(w, "url is required", http.StatusBadRequest) return } if _, err := url.ParseRequestURI(payload.URL); err != nil { http.Error(w, "invalid url", http.StatusBadRequest) return } payload.OutDir = s.resolveOutDir(payload.OutDir) if payload.OutDir == "" { http.Error(w, "out_dir is required", http.StatusBadRequest) return } id, err := s.queue.Enqueue(payload) if err != nil { log.Printf("failed to enqueue request: %v", err) http.Error(w, "failed to queue request", http.StatusInternalServerError) return } log.Printf("queued video %s -> %s (%s)", payload.URL, payload.OutDir, id) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(map[string]string{ "status": "queued", "id": id, }) } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.Header().Set("Allow", http.MethodGet) http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if !s.authorize(r.Header.Get("Authorization")) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } items := s.queue.Snapshot() w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(map[string]interface{}{ "count": len(items), "items": items, }); err != nil { log.Printf("status encode error: %v", err) } } func (s *Server) authorize(header string) bool { required := s.authToken if required == "" { return true } if header == "" { return false } parts := strings.SplitN(header, " ", 2) if len(parts) != 2 { return false } if !strings.EqualFold(parts[0], "Bearer") { return false } return strings.TrimSpace(parts[1]) == required } func (s *Server) resolveOutDir(raw string) string { root := s.defaultOutDir if raw == "" { return root } if filepath.IsAbs(raw) { return filepath.Clean(raw) } if root == "" { return filepath.Clean(raw) } return filepath.Join(root, raw) }