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 }