package api import ( "context" "encoding/json" "errors" "io" "net/http" "jukebox_maker/internal/config" "jukebox_maker/internal/copier" "jukebox_maker/internal/disk" ) func (s *Server) copyOptions(cfg *config.Config, diskInfo disk.DiskInfo, overwriteMode config.OverwriteMode) copier.Options { opts := copier.Options{ DiskID: diskInfo.DiskID, MountPath: diskInfo.MountPath, MediaPath: cfg.MediaPath, SourceRules: cfg.Sources, AllowedExtensions: cfg.EffectiveAllowedExtensions(), OverwriteMode: overwriteMode, } if p := diskInfo.Profile; p != nil { opts.DestFolder = p.DestFolder opts.ReserveFreeGB = p.ReserveFreeGB opts.FileSelectMode = config.FileSelectMode(p.FileSelectMode) opts.Transcode = p.Transcode opts.ShuffleDepth = p.ShuffleDepth } else { opts.DestFolder = cfg.DestFolder opts.ReserveFreeGB = cfg.ReserveFreeGB opts.FileSelectMode = cfg.FileSelectMode } return opts } func hasEnabledSources(cfg *config.Config) bool { for _, src := range cfg.Sources { if src.Enabled { return true } } return false } func decodeCopyMode(r *http.Request, fallback config.OverwriteMode) (config.OverwriteMode, error) { var req struct { Mode string `json:"mode"` } if r.Body != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) { return "", err } } switch req.Mode { case "", "add": return config.OverwriteSkip, nil case "replace": return config.OverwriteDelete, nil default: return fallback, errors.New("invalid copy mode") } } func (s *Server) handleCopyStart(w http.ResponseWriter, r *http.Request) { diskID := r.PathValue("diskID") diskInfo, ok := s.deps.Watcher.DiskByID(diskID) if !ok || diskInfo.State != disk.DiskKnown { jsonErr(w, http.StatusUnprocessableEntity, "no initialized disk connected") return } cfg := s.deps.Config if !hasEnabledSources(cfg) { jsonErr(w, http.StatusUnprocessableEntity, "no source folders selected") return } overwriteMode, err := decodeCopyMode(r, cfg.OverwriteMode) if err != nil { if err.Error() == "invalid copy mode" { jsonErr(w, http.StatusBadRequest, err.Error()) return } jsonErr(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } reserveGB := cfg.ReserveFreeGB if diskInfo.Profile != nil { reserveGB = diskInfo.Profile.ReserveFreeGB } if diskInfo.FreeBytes <= int64(reserveGB*1e9) { jsonErr(w, http.StatusUnprocessableEntity, "free space is below reserve threshold") return } opts := s.copyOptions(cfg, diskInfo, overwriteMode) taskID, err := s.deps.Copier.Start(context.Background(), opts) if err != nil { switch err.Error() { case "copy already running": jsonErr(w, http.StatusConflict, err.Error()) default: jsonErr(w, http.StatusUnprocessableEntity, err.Error()) } return } w.WriteHeader(http.StatusAccepted) jsonOK(w, map[string]string{"task_id": taskID}) } func (s *Server) handleCopyStartSelected(w http.ResponseWriter, r *http.Request) { cfg := s.deps.Config if !hasEnabledSources(cfg) { jsonErr(w, http.StatusUnprocessableEntity, "no source folders selected") return } var req struct { MountPath string `json:"mount_path"` Mode string `json:"mode"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonErr(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } diskInfo, err := s.deps.ProbeDisk(req.MountPath) if err != nil { jsonErr(w, http.StatusBadRequest, err.Error()) return } if diskInfo.State != disk.DiskKnown { jsonErr(w, http.StatusUnprocessableEntity, "selected directory is not an initialized jukebox disk") return } overwriteMode := cfg.OverwriteMode switch req.Mode { case "", "add": overwriteMode = config.OverwriteSkip case "replace": overwriteMode = config.OverwriteDelete default: jsonErr(w, http.StatusBadRequest, "invalid copy mode") return } reserveGB := cfg.ReserveFreeGB if diskInfo.Profile != nil { reserveGB = diskInfo.Profile.ReserveFreeGB } if diskInfo.FreeBytes <= int64(reserveGB*1e9) { jsonErr(w, http.StatusUnprocessableEntity, "free space is below reserve threshold") return } if s.deps.OnDiskInit != nil { s.deps.OnDiskInit(diskInfo.MountPath, diskInfo.DiskID) } taskID, err := s.deps.Copier.Start(context.Background(), s.copyOptions(cfg, diskInfo, overwriteMode)) if err != nil { switch err.Error() { case "copy already running": jsonErr(w, http.StatusConflict, err.Error()) default: jsonErr(w, http.StatusUnprocessableEntity, err.Error()) } return } w.WriteHeader(http.StatusAccepted) jsonOK(w, map[string]string{"task_id": taskID}) } func (s *Server) handleCopyCancel(w http.ResponseWriter, r *http.Request) { diskID := r.PathValue("diskID") s.deps.Copier.Cancel(diskID) jsonOK(w, map[string]bool{"ok": true}) } func (s *Server) handleTaskGet(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") t, ok := s.deps.Tasks.Get(id) if !ok { jsonErr(w, http.StatusNotFound, "task not found") return } jsonOK(w, t) }