1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
package admin
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"thehouseoficarus/internal/world"
)
type bulkDeleteRequest struct {
IDs []int `json:"ids"`
}
func (s *AdminServer) handleBulkDelete(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 bulkDeleteRequest
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
}
// Duplicate IDs in one request would delete the same file twice: the
// second os.Remove fails after the room is already gone, and the
// handler aborts without pushing an undo record. Reject up front.
seenIDs := make(map[int]bool, len(req.IDs))
for _, id := range req.IDs {
if seenIDs[id] {
writeJSON(w, map[string]any{"error": fmt.Sprintf("duplicate room id %d in request", id)})
return
}
seenIDs[id] = true
}
type roomSnapshot struct {
id int
path string
oldContent []byte
}
snapshots := make([]roomSnapshot, 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
}
oldContent, snapErr := snapshotFile(path)
if snapErr != nil {
writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room %d: %v", id, snapErr)})
return
}
snapshots = append(snapshots, roomSnapshot{id: id, path: path, oldContent: oldContent})
}
deletedSet := make(map[int]bool, len(req.IDs))
for _, snap := range snapshots {
s.detachRoomFromCourse(snap.id)
if err := os.Remove(snap.path); err != nil {
// All-or-nothing: restore every room deleted so far so a failed
// bulk delete never strands rooms without an undo record.
for _, done := range snapshots {
if deletedSet[done.id] {
if werr := os.WriteFile(done.path, done.oldContent, 0644); werr != nil {
log.Printf("bulk-delete rollback of room %d: %v", done.id, werr)
}
}
}
writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room %d: %v", snap.id, err)})
return
}
deletedSet[snap.id] = true
}
s.world.RebuildRoomIndex(s.dataDir)
for _, snap := range snapshots {
s.world.ClearRoomState(snap.id)
if s.evictRoom != nil {
s.evictRoom(snap.id)
}
}
allIDs, _ := listRoomIDs(s.dataDir)
var extraFiles []ExtraFile
var exitCleaned int
for _, otherID := range allIDs {
if deletedSet[otherID] {
continue
}
otherRoom, loadErr := s.world.LoadRoom(otherID)
if loadErr != nil {
continue
}
changed := false
for _, dir := range world.ExitOrder {
exit, exists := otherRoom.Exits[dir]
if !exists || !deletedSet[exit.Room] {
continue
}
delete(otherRoom.Exits, dir)
changed = true
}
if changed {
otherPath, pathOk := s.world.GetRoomPath(otherID)
if pathOk {
oldExtra, _ := snapshotFile(otherPath)
newContent, writeErr := writeYAMLFile(otherPath, otherRoom)
if writeErr != nil {
continue
}
extraFiles = append(extraFiles, ExtraFile{
FilePath: otherPath,
OldContent: oldExtra,
NewContent: newContent,
})
exitCleaned++
}
}
}
for i := 1; i < len(snapshots); i++ {
extraFiles = append(extraFiles, ExtraFile{
FilePath: snapshots[i].path,
OldContent: snapshots[i].oldContent,
IsDelete: true,
})
}
s.undoStack.Push(ChangeDesc{
Description: fmt.Sprintf("delete %d rooms (cleaned %d exits)", len(req.IDs), exitCleaned),
FilePath: snapshots[0].path,
OldContent: snapshots[0].oldContent,
IsDelete: true,
ExtraFiles: extraFiles,
})
writeJSON(w, map[string]any{"ok": true, "count": len(req.IDs)})
}
|