diff options
| author | historia <[not public]> | 2026-06-29 22:31:11 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-29 22:31:11 -0400 |
| commit | 069d3c1c81d71042706372df153254487a765dfc (patch) | |
| tree | d83a4a3954d2b8304f49d83e365f4c2b075b91eb /internal/admin/undo.go | |
| parent | 6c5271fb085b4571a10f106391974c249cd84317 (diff) | |
| download | thehouseoficarus-069d3c1c81d71042706372df153254487a765dfc.tar.gz | |
feat: wip web admin for mapping and crud
Diffstat (limited to 'internal/admin/undo.go')
| -rw-r--r-- | internal/admin/undo.go | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/internal/admin/undo.go b/internal/admin/undo.go new file mode 100644 index 0000000..39e8c4e --- /dev/null +++ b/internal/admin/undo.go @@ -0,0 +1,247 @@ +package admin + +import ( + "encoding/json" + "log" + "os" + "path/filepath" + "sync" + "time" +) + +type ChangeDesc struct { + Time string `json:"time"` + Description string `json:"description"` + FilePath string `json:"file_path"` + NewFilePath string `json:"new_file_path,omitempty"` + OldContent []byte `json:"old_content"` + NewContent []byte `json:"new_content"` + IsDelete bool `json:"is_delete"` + IsCreate bool `json:"is_create"` +} + +type UndoInfo struct { + CanUndo bool `json:"can_undo"` + CanRedo bool `json:"can_redo"` + UndoDesc string `json:"undo_desc"` + RedoDesc string `json:"redo_desc"` + StackSize int `json:"stack_size"` + RedoSize int `json:"redo_size"` +} + +type UndoStack struct { + mu sync.Mutex + dataDir string + history []ChangeDesc + redo []ChangeDesc + maxSize int + filePath string +} + +func NewUndoStack(dataDir string) *UndoStack { + us := &UndoStack{ + dataDir: dataDir, + maxSize: 100, + filePath: filepath.Join(dataDir, ".admin_history.yaml"), + } + us.load() + return us +} + +func (us *UndoStack) Push(desc ChangeDesc) { + us.mu.Lock() + defer us.mu.Unlock() + desc.Time = time.Now().Format(time.RFC3339) + us.history = append(us.history, desc) + us.redo = nil + if len(us.history) > us.maxSize { + us.history = us.history[len(us.history)-us.maxSize:] + } + us.save() +} + +func (us *UndoStack) Undo() *ChangeDesc { + us.mu.Lock() + defer us.mu.Unlock() + if len(us.history) == 0 { + return nil + } + last := us.history[len(us.history)-1] + us.history = us.history[:len(us.history)-1] + + if last.NewFilePath != "" { + os.Remove(last.NewFilePath) + } + if last.IsDelete { + if err := os.WriteFile(last.FilePath, last.OldContent, 0644); err != nil { + log.Printf("undo: failed to restore deleted file %s: %v", last.FilePath, err) + return nil + } + } else if last.IsCreate { + os.Remove(last.FilePath) + } else { + if last.NewFilePath != "" { + if err := os.WriteFile(last.FilePath, last.OldContent, 0644); err != nil { + log.Printf("undo: failed to revert move %s: %v", last.FilePath, err) + return nil + } + } else { + if err := os.WriteFile(last.FilePath, last.OldContent, 0644); err != nil { + log.Printf("undo: failed to revert file %s: %v", last.FilePath, err) + return nil + } + } + } + + us.redo = append(us.redo, last) + us.save() + return &last +} + +func (us *UndoStack) Redo() *ChangeDesc { + us.mu.Lock() + defer us.mu.Unlock() + if len(us.redo) == 0 { + return nil + } + last := us.redo[len(us.redo)-1] + us.redo = us.redo[:len(us.redo)-1] + + if last.NewFilePath != "" { + os.Remove(last.FilePath) + } + if last.IsCreate { + if last.NewFilePath != "" { + if err := os.WriteFile(last.NewFilePath, last.NewContent, 0644); err != nil { + log.Printf("redo: failed to recreate file %s: %v", last.NewFilePath, err) + return nil + } + } else { + if err := os.WriteFile(last.FilePath, last.NewContent, 0644); err != nil { + log.Printf("redo: failed to recreate file %s: %v", last.FilePath, err) + return nil + } + } + } else if last.IsDelete { + os.Remove(last.FilePath) + } else { + if last.NewFilePath != "" { + if err := os.WriteFile(last.NewFilePath, last.NewContent, 0644); err != nil { + log.Printf("redo: failed to reapply move %s: %v", last.NewFilePath, err) + return nil + } + } else { + if err := os.WriteFile(last.FilePath, last.NewContent, 0644); err != nil { + log.Printf("redo: failed to reapply file %s: %v", last.FilePath, err) + return nil + } + } + } + + us.history = append(us.history, last) + us.save() + return &last +} + +func (us *UndoStack) Info() UndoInfo { + us.mu.Lock() + defer us.mu.Unlock() + info := UndoInfo{ + StackSize: len(us.history), + RedoSize: len(us.redo), + } + if len(us.history) > 0 { + info.CanUndo = true + info.UndoDesc = us.history[len(us.history)-1].Description + } + if len(us.redo) > 0 { + info.CanRedo = true + info.RedoDesc = us.redo[len(us.redo)-1].Description + } + return info +} + +func (us *UndoStack) save() { + type entry struct { + Time string `json:"time"` + Description string `json:"description"` + FilePath string `json:"file_path"` + NewFilePath string `json:"new_file_path,omitempty"` + OldContent string `json:"old_content"` + NewContent string `json:"new_content"` + IsDelete bool `json:"is_delete"` + IsCreate bool `json:"is_create"` + } + type saveData struct { + History []entry `json:"history"` + Redo []entry `json:"redo"` + } + toEntries := func(changes []ChangeDesc) []entry { + var entries []entry + for _, c := range changes { + entries = append(entries, entry{ + Time: c.Time, + Description: c.Description, + FilePath: c.FilePath, + NewFilePath: c.NewFilePath, + OldContent: string(c.OldContent), + NewContent: string(c.NewContent), + IsDelete: c.IsDelete, + IsCreate: c.IsCreate, + }) + } + return entries + } + data := saveData{ + History: toEntries(us.history), + Redo: toEntries(us.redo), + } + b, err := json.Marshal(data) + if err != nil { + log.Printf("undo: failed to marshal history: %v", err) + return + } + os.WriteFile(us.filePath, b, 0644) +} + +func (us *UndoStack) load() { + data, err := os.ReadFile(us.filePath) + if err != nil { + return + } + type entry struct { + Time string `json:"time"` + Description string `json:"description"` + FilePath string `json:"file_path"` + NewFilePath string `json:"new_file_path,omitempty"` + OldContent string `json:"old_content"` + NewContent string `json:"new_content"` + IsDelete bool `json:"is_delete"` + IsCreate bool `json:"is_create"` + } + var saveData struct { + History []entry `json:"history"` + Redo []entry `json:"redo"` + } + if err := json.Unmarshal(data, &saveData); err != nil { + return + } + fromEntries := func(entries []entry) []ChangeDesc { + var changes []ChangeDesc + for _, e := range entries { + changes = append(changes, ChangeDesc{ + Time: e.Time, + Description: e.Description, + FilePath: e.FilePath, + NewFilePath: e.NewFilePath, + OldContent: []byte(e.OldContent), + NewContent: []byte(e.NewContent), + IsDelete: e.IsDelete, + IsCreate: e.IsCreate, + }) + } + return changes + } + us.history = fromEntries(saveData.History) + us.redo = fromEntries(saveData.Redo) +} |
