subsyt/internal/server/server.go

210 lines
4.4 KiB
Go

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)
}