aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/api_rooms_bulk.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/api_rooms_bulk.go')
-rw-r--r--internal/admin/api_rooms_bulk.go214
1 files changed, 214 insertions, 0 deletions
diff --git a/internal/admin/api_rooms_bulk.go b/internal/admin/api_rooms_bulk.go
new file mode 100644
index 0000000..34ca209
--- /dev/null
+++ b/internal/admin/api_rooms_bulk.go
@@ -0,0 +1,214 @@
+package admin
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+)
+
+type bulkEditPatch struct {
+ Color *string `json:"color"`
+ BlockTransport *bool `json:"block_transport"`
+ Dir *string `json:"dir"`
+ Hazard *string `json:"hazard"`
+}
+
+type bulkEditRequest struct {
+ Patch bulkEditPatch `json:"patch"`
+ IDs []int `json:"ids"`
+}
+
+func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ body, readErr := io.ReadAll(r.Body)
+ r.Body.Close()
+ if readErr != nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)})
+ return
+ }
+
+ var req bulkEditRequest
+ if err := json.Unmarshal(body, &req); err != nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ return
+ }
+
+ if len(req.IDs) == 0 {
+ writeJSON(w, map[string]any{"error": "no room ids"})
+ return
+ }
+
+ if isEmptyPatch(req.Patch) {
+ writeJSON(w, map[string]any{"error": "no patch fields"})
+ return
+ }
+
+ patch := req.Patch
+
+ roomPaths := make([]string, 0, len(req.IDs))
+ for _, id := range req.IDs {
+ path, ok := s.world.GetRoomPath(id)
+ if !ok {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ return
+ }
+ roomPaths = append(roomPaths, path)
+ }
+
+ type roomWrite struct {
+ oldContent []byte
+ newContent []byte
+ oldPath string
+ newPath string
+ wasMoved bool
+ }
+
+ writes := make([]roomWrite, 0, len(req.IDs))
+ dirMovePaths := make(map[string]struct{})
+
+ for i, id := range req.IDs {
+ path := roomPaths[i]
+
+ oldContent, snapErr := snapshotFile(path)
+ if snapErr != nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room %d: %v", id, snapErr)})
+ return
+ }
+
+ data, readErr := os.ReadFile(path)
+ if readErr != nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("read room %d: %v", id, readErr)})
+ return
+ }
+
+ m, yamlErr := yamlToMap(data)
+ if yamlErr != nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("parse room %d: %v", id, yamlErr)})
+ return
+ }
+
+ if patch.Color != nil {
+ m["color"] = *patch.Color
+ }
+ if patch.BlockTransport != nil {
+ m["block_transport"] = *patch.BlockTransport
+ }
+ if patch.Hazard != nil {
+ if *patch.Hazard == "" {
+ delete(m, "hazard")
+ } else {
+ m["hazard"] = *patch.Hazard
+ }
+ }
+
+ if patch.Dir != nil && *patch.Dir != "" {
+ destDir := filepath.Join(s.dataDir, "rooms", *patch.Dir)
+ if _, err := os.Stat(destDir); os.IsNotExist(err) {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("directory does not exist: %s", *patch.Dir)})
+ return
+ }
+
+ fileName := strconv.Itoa(id) + ".yaml"
+ newPath := filepath.Join(destDir, fileName)
+ if newPath != path {
+ if _, err := os.Stat(newPath); err == nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d already exists in target directory", id)})
+ return
+ }
+ }
+
+ newContent, writeErr := writeMapAsYAML(newPath, m)
+ if writeErr != nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("write room %d: %v", id, writeErr)})
+ return
+ }
+
+ writes = append(writes, roomWrite{
+ oldContent: oldContent,
+ newContent: newContent,
+ oldPath: path,
+ newPath: newPath,
+ wasMoved: true,
+ })
+ dirMovePaths[path] = struct{}{}
+ } else {
+ newContent, writeErr := writeMapAsYAML(path, m)
+ if writeErr != nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("write room %d: %v", id, writeErr)})
+ return
+ }
+
+ writes = append(writes, roomWrite{
+ oldContent: oldContent,
+ newContent: newContent,
+ oldPath: path,
+ newPath: path,
+ wasMoved: false,
+ })
+ }
+ }
+
+ for _, rw := range writes {
+ if rw.wasMoved {
+ if _, alreadyRemoved := dirMovePaths[rw.oldPath]; alreadyRemoved {
+ delete(dirMovePaths, rw.oldPath)
+ if err := os.Remove(rw.oldPath); err != nil {
+ writeJSON(w, map[string]any{"error": fmt.Sprintf("remove old room: %v", err)})
+ return
+ }
+ }
+ }
+ }
+
+ s.world.RebuildRoomIndex(s.dataDir)
+
+ var desc string
+ if patch.Dir != nil && *patch.Dir != "" {
+ desc = fmt.Sprintf("bulk edit %d rooms (move to %s)", len(req.IDs), *patch.Dir)
+ } else {
+ desc = fmt.Sprintf("bulk edit %d rooms", len(req.IDs))
+ }
+
+ var extraFiles []ExtraFile
+ var firstDesc ChangeDesc
+ for i, rw := range writes {
+ ef := ExtraFile{
+ FilePath: rw.oldPath,
+ OldContent: rw.oldContent,
+ NewContent: rw.newContent,
+ }
+ if rw.wasMoved {
+ ef.NewFilePath = rw.newPath
+ }
+ if i == 0 {
+ firstDesc = ChangeDesc{
+ Description: desc,
+ FilePath: rw.oldPath,
+ OldContent: rw.oldContent,
+ NewContent: rw.newContent,
+ }
+ if rw.wasMoved {
+ firstDesc.NewFilePath = rw.newPath
+ }
+ } else {
+ extraFiles = append(extraFiles, ef)
+ }
+ }
+
+ firstDesc.ExtraFiles = extraFiles
+ s.undoStack.Push(firstDesc)
+
+ writeJSON(w, map[string]any{"ok": true, "count": len(req.IDs)})
+}
+
+func isEmptyPatch(p bulkEditPatch) bool {
+ return p.Color == nil && p.BlockTransport == nil && p.Dir == nil && p.Hazard == nil
+}