package admin import ( "io" "net/http" "os" "path/filepath" "strings" ) func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var body struct { Path string `json:"path"` NewName string `json:"new_name"` } if err := readJSON(r, &body); err != nil || body.Path == "" || body.NewName == "" { writeJSON(w, map[string]any{"error": "missing path or new_name"}) return } newName := strings.TrimSpace(body.NewName) if newName == "" || newName == "." || newName == ".." || strings.ContainsAny(newName, "/\\") { writeJSON(w, map[string]any{"error": "invalid file name"}) return } for _, c := range newName { if c < 32 || c == 127 { writeJSON(w, map[string]any{"error": "file name contains invalid characters"}) return } } relPath := strings.TrimPrefix(body.Path, "/") relPath = strings.TrimPrefix(relPath, "data/") oldAbs := filepath.Join(s.dataDir, relPath) oldContent, err := snapshotFile(oldAbs) if err != nil { writeJSON(w, map[string]any{"error": "file not found: " + relPath}) return } newAbs := filepath.Join(filepath.Dir(oldAbs), newName) if _, err := os.Stat(newAbs); err == nil { writeJSON(w, map[string]any{"error": "a file named " + newName + " already exists"}) return } if err := os.WriteFile(newAbs, oldContent, 0644); err != nil { writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) return } if err := os.Remove(oldAbs); err != nil { writeJSON(w, map[string]any{"error": "remove error: " + err.Error()}) return } newRelPath := filepath.Join(filepath.Dir(relPath), newName) if filepath.Dir(relPath) == "." { newRelPath = newName } s.undoStack.Push(ChangeDesc{ Description: "Renamed file " + relPath + " to " + newRelPath, FilePath: oldAbs, NewFilePath: newAbs, OldContent: oldContent, NewContent: oldContent, }) writeJSON(w, map[string]any{"ok": true, "path": newRelPath}) } func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { relPath := r.URL.Query().Get("path") relPath = strings.TrimPrefix(relPath, "/") relPath = strings.TrimPrefix(relPath, "data/") switch r.Method { case http.MethodGet: if relPath == "" { entries, err := os.ReadDir(s.dataDir) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } var files []map[string]any for _, e := range entries { files = append(files, map[string]any{ "name": e.Name(), "dir": e.IsDir(), }) } writeJSON(w, files) return } fullPath := filepath.Join(s.dataDir, relPath) info, err := os.Stat(fullPath) if err != nil { writeJSON(w, map[string]any{"error": "not found: " + relPath}) return } if info.IsDir() { entries, err := os.ReadDir(fullPath) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } var files []map[string]any for _, e := range entries { files = append(files, map[string]any{ "name": e.Name(), "dir": e.IsDir(), }) } writeJSON(w, files) return } data, err := os.ReadFile(fullPath) if err != nil { writeJSON(w, map[string]any{"error": "read error: " + err.Error()}) return } writeJSON(w, map[string]any{ "name": info.Name(), "content": string(data), "path": relPath, }) case http.MethodPut, http.MethodPost: if relPath == "" { writeJSON(w, map[string]any{"error": "missing path"}) return } fullPath := filepath.Join(s.dataDir, relPath) body, err := io.ReadAll(r.Body) if err != nil { writeJSON(w, map[string]any{"error": "read body: " + err.Error()}) return } var oldContent []byte if existing, err := snapshotFile(fullPath); err == nil { oldContent = existing } dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { writeJSON(w, map[string]any{"error": "mkdir: " + err.Error()}) return } if err := os.WriteFile(fullPath, body, 0644); err != nil { writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) return } isCreate := oldContent == nil desc := "Updated file " + relPath if isCreate { desc = "Created file " + relPath } s.undoStack.Push(ChangeDesc{ Description: desc, FilePath: fullPath, OldContent: oldContent, NewContent: body, IsCreate: isCreate, }) writeJSON(w, map[string]any{"ok": true, "path": relPath}) case http.MethodDelete: if relPath == "" { writeJSON(w, map[string]any{"error": "missing path"}) return } fullPath := filepath.Join(s.dataDir, relPath) oldContent, _ := snapshotFile(fullPath) if err := os.Remove(fullPath); err != nil { writeJSON(w, map[string]any{"error": "delete error: " + err.Error()}) return } s.undoStack.Push(ChangeDesc{ Description: "Deleted file " + relPath, FilePath: fullPath, OldContent: oldContent, IsDelete: true, }) writeJSON(w, map[string]any{"ok": true, "path": relPath}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } }