210 lines
4.4 KiB
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 = "0.0.0.0:6901"
|
|
|
|
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)
|
|
}
|