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.json"), } 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) }