Improve disk UI and build performance
This commit is contained in:
@@ -2,8 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"jukebox_maker/internal/config"
|
||||
"jukebox_maker/internal/copier"
|
||||
"jukebox_maker/internal/disk"
|
||||
)
|
||||
@@ -12,19 +16,47 @@ 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 known disk connected")
|
||||
jsonErr(w, http.StatusUnprocessableEntity, "no initialized disk connected")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := s.deps.Config
|
||||
var enabledSources []string
|
||||
hasEnabledSources := false
|
||||
for _, src := range cfg.Sources {
|
||||
if src.Enabled {
|
||||
enabledSources = append(enabledSources, src.Path)
|
||||
hasEnabledSources = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(enabledSources) == 0 {
|
||||
jsonErr(w, http.StatusUnprocessableEntity, "no sources enabled")
|
||||
if !hasEnabledSources {
|
||||
jsonErr(w, http.StatusUnprocessableEntity, "no source folders selected")
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
jsonErr(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
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
|
||||
}
|
||||
|
||||
reserveBytes := int64(cfg.ReserveFreeGB * 1e9)
|
||||
if diskInfo.FreeBytes <= reserveBytes {
|
||||
jsonErr(w, http.StatusUnprocessableEntity, "free space is below reserve threshold")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -33,9 +65,9 @@ func (s *Server) handleCopyStart(w http.ResponseWriter, r *http.Request) {
|
||||
MountPath: diskInfo.MountPath,
|
||||
MediaPath: s.deps.MediaPath,
|
||||
DestFolder: cfg.DestFolder,
|
||||
EnabledSources: enabledSources,
|
||||
SourceRules: cfg.Sources,
|
||||
ReserveFreeGB: cfg.ReserveFreeGB,
|
||||
OverwriteMode: cfg.OverwriteMode,
|
||||
OverwriteMode: overwriteMode,
|
||||
FileSelectMode: cfg.FileSelectMode,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"jukebox_maker/internal/disk"
|
||||
)
|
||||
@@ -14,6 +15,7 @@ func (s *Server) handleDiskStatus(w http.ResponseWriter, r *http.Request) {
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
FreeBytes int64 `json:"free_bytes"`
|
||||
MountPath string `json:"mount_path"`
|
||||
LastCopiedAt string `json:"last_copied_at,omitempty"`
|
||||
ActiveTaskID string `json:"active_task_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -28,6 +30,9 @@ func (s *Server) handleDiskStatus(w http.ResponseWriter, r *http.Request) {
|
||||
MountPath: info.MountPath,
|
||||
}
|
||||
if info.DiskID != "" {
|
||||
if lastCopiedAt, ok, err := s.deps.Copier.LastCopiedAt(info.DiskID); err == nil && ok {
|
||||
item.LastCopiedAt = lastCopiedAt.Format(time.RFC3339)
|
||||
}
|
||||
if t, ok := s.deps.Tasks.ActiveTaskByDisk(info.DiskID); ok {
|
||||
item.ActiveTaskID = t.ID
|
||||
}
|
||||
|
||||
@@ -1,26 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Server) handleSources(w http.ResponseWriter, r *http.Request) {
|
||||
entries, err := os.ReadDir(s.deps.MediaPath)
|
||||
relPath, err := normalizeSourcePath(r.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
jsonOK(w, map[string][]string{"items": {}})
|
||||
jsonErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var items []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() && e.Name()[0] != '.' {
|
||||
items = append(items, e.Name())
|
||||
}
|
||||
}
|
||||
if items == nil {
|
||||
items = []string{}
|
||||
absPath := s.deps.MediaPath
|
||||
if relPath != "" {
|
||||
absPath = filepath.Join(absPath, relPath)
|
||||
}
|
||||
|
||||
jsonOK(w, map[string][]string{"items": items})
|
||||
entries, err := os.ReadDir(absPath)
|
||||
if err != nil {
|
||||
jsonOK(w, map[string]any{"path": relPath, "items": []map[string]string{}})
|
||||
return
|
||||
}
|
||||
|
||||
type item struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
var items []item
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
childPath := e.Name()
|
||||
if relPath != "" {
|
||||
childPath = filepath.Join(relPath, childPath)
|
||||
}
|
||||
items = append(items, item{
|
||||
Name: e.Name(),
|
||||
Path: filepath.ToSlash(childPath),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
|
||||
})
|
||||
|
||||
jsonOK(w, map[string]any{
|
||||
"path": relPath,
|
||||
"items": items,
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeSourcePath(raw string) (string, error) {
|
||||
raw, _ = url.QueryUnescape(raw)
|
||||
raw = strings.TrimSpace(raw)
|
||||
raw = filepath.ToSlash(raw)
|
||||
raw = strings.TrimPrefix(raw, "/")
|
||||
if raw == "" || raw == "." {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
clean := filepath.Clean(raw)
|
||||
clean = filepath.ToSlash(clean)
|
||||
if clean == "." {
|
||||
return "", nil
|
||||
}
|
||||
if clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return "", errors.New("invalid source path")
|
||||
}
|
||||
return clean, nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
type Deps struct {
|
||||
Config *config.Config
|
||||
ConfigPath string
|
||||
Version string
|
||||
Watcher *watcher.Watcher
|
||||
Copier *copier.Copier
|
||||
Tasks *task.Store
|
||||
@@ -77,7 +78,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, s.settings, map[string]any{"Title": "Настройки", "Page": "settings"})
|
||||
s.render(w, s.settings, map[string]any{"Title": "Settings", "Page": "settings"})
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -86,7 +87,13 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) render(w http.ResponseWriter, tmpl *template.Template, data any) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := tmpl.ExecuteTemplate(w, "layout", data); err != nil {
|
||||
payload := map[string]any{"Version": s.deps.Version}
|
||||
if incoming, ok := data.(map[string]any); ok {
|
||||
for k, v := range incoming {
|
||||
payload[k] = v
|
||||
}
|
||||
}
|
||||
if err := tmpl.ExecuteTemplate(w, "layout", payload); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user