package admin import ( "bytes" "fmt" "log" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) func readYAMLFile(dataDir, subdir, id string) ([]byte, string, error) { path := filepath.Join(dataDir, subdir, id+".yaml") data, err := os.ReadFile(path) return data, path, err } func writeYAMLFile(path string, data any) ([]byte, error) { var buf bytes.Buffer enc := yaml.NewEncoder(&buf) enc.SetIndent(2) if err := enc.Encode(data); err != nil { return nil, err } enc.Close() content := buf.Bytes() if err := os.WriteFile(path, content, 0644); err != nil { return nil, err } return content, nil } func findYAMLFileInSubdirs(dataDir, subdir, id string) ([]byte, string, error) { base := filepath.Join(dataDir, subdir) entries, err := os.ReadDir(base) if err != nil { return nil, "", err } for _, e := range entries { fullPath := filepath.Join(base, e.Name(), id+".yaml") if data, err := os.ReadFile(fullPath); err == nil { return data, fullPath, nil } subPath := filepath.Join(base, e.Name()) subEntries, err := os.ReadDir(subPath) if err != nil { continue } for _, se := range subEntries { fullPath := filepath.Join(subPath, se.Name(), id+".yaml") if data, err := os.ReadFile(fullPath); err == nil { return data, fullPath, nil } } } return nil, "", os.ErrNotExist } func listYAMLFiles(dataDir, subdir string) ([]string, error) { var ids []string base := filepath.Join(dataDir, subdir) entries, err := os.ReadDir(base) if err != nil { return nil, err } for _, e := range entries { if !e.IsDir() && filepath.Ext(e.Name()) == ".yaml" { ids = append(ids, e.Name()[:len(e.Name())-5]) } } for _, e := range entries { if e.IsDir() { subPath := filepath.Join(base, e.Name()) subEntries, err := os.ReadDir(subPath) if err != nil { continue } for _, se := range subEntries { if !se.IsDir() && filepath.Ext(se.Name()) == ".yaml" { ids = append(ids, se.Name()[:len(se.Name())-5]) } } } } return ids, nil } type YAMLTree struct { Root []string `json:"root"` Dirs map[string][]string `json:"dirs"` } func listYAMLTree(dataDir, subdir string) (*YAMLTree, error) { tree := &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} base := filepath.Join(dataDir, subdir) entries, err := os.ReadDir(base) if err != nil { return nil, err } for _, e := range entries { if !e.IsDir() && filepath.Ext(e.Name()) == ".yaml" { tree.Root = append(tree.Root, e.Name()[:len(e.Name())-5]) } } for _, e := range entries { if e.IsDir() { subPath := filepath.Join(base, e.Name()) subEntries, err := os.ReadDir(subPath) if err != nil { continue } var ids []string for _, se := range subEntries { if !se.IsDir() && filepath.Ext(se.Name()) == ".yaml" { ids = append(ids, se.Name()[:len(se.Name())-5]) } } if len(ids) > 0 { tree.Dirs[e.Name()] = ids } else { tree.Dirs[e.Name()] = []string{} } } } return tree, nil } func listYAMLFilesDeep(dataDir, subdir string) ([]string, error) { var ids []string base := filepath.Join(dataDir, subdir) err := filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() && filepath.Ext(d.Name()) == ".yaml" { rel, _ := filepath.Rel(base, path) id := rel[:len(rel)-5] ids = append(ids, id) } return nil }) return ids, err } func backupFile(path string) []byte { data, err := os.ReadFile(path) if err != nil { log.Printf("backup: WARNING failed to read %s: %v (undo will not be able to restore this file)", path, err) return nil } return data } func snapshotFile(path string) ([]byte, error) { return os.ReadFile(path) } func findEntityPath(dataDir, subdir, id string) (string, error) { _, path, err := readYAMLFile(dataDir, subdir, id) if err == nil { return path, nil } _, path, err = findYAMLFileInSubdirs(dataDir, subdir, id) return path, err } func moveEntityFile(dataDir, subdir, id, targetDir string) (string, string, []byte, error) { oldPath, err := findEntityPath(dataDir, subdir, id) if err != nil { return "", "", nil, fmt.Errorf("entity not found: %s/%s", subdir, id) } data, err := os.ReadFile(oldPath) if err != nil { return "", "", nil, fmt.Errorf("read: %w", err) } var newPath string if targetDir == "" { newPath = filepath.Join(dataDir, subdir, id+".yaml") } else { destDir := filepath.Join(dataDir, subdir, targetDir) if _, err := os.Stat(destDir); os.IsNotExist(err) { return "", "", nil, fmt.Errorf("directory does not exist: %s", targetDir) } newPath = filepath.Join(destDir, id+".yaml") } if oldPath == newPath { return "", "", nil, fmt.Errorf("already in target directory") } if _, err := os.Stat(newPath); err == nil { return "", "", nil, fmt.Errorf("entity already exists in target directory") } if err := os.WriteFile(newPath, data, 0644); err != nil { return "", "", nil, fmt.Errorf("write: %w", err) } if err := os.Remove(oldPath); err != nil { return "", "", nil, fmt.Errorf("remove old: %w", err) } return oldPath, newPath, data, nil } func createEntityDir(dataDir, subdir, name string) error { name = strings.TrimSpace(name) if name == "" || strings.ContainsAny(name, "/\\") || name == "." || name == ".." { return fmt.Errorf("invalid directory name") } dirPath := filepath.Join(dataDir, subdir, name) if _, err := os.Stat(dirPath); err == nil { return fmt.Errorf("directory already exists") } return os.Mkdir(dirPath, 0755) } func deleteEntityDir(dataDir, subdir, dir string) ([]ChangeDesc, error) { dir = strings.TrimSpace(dir) if dir == "" || strings.ContainsAny(dir, "/\\") || dir == "." || dir == ".." { return nil, fmt.Errorf("invalid directory") } srcDir := filepath.Join(dataDir, subdir, dir) entries, err := os.ReadDir(srcDir) if err != nil { return nil, fmt.Errorf("directory not found: %s", dir) } var changes []ChangeDesc for _, e := range entries { if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { continue } oldPath := filepath.Join(srcDir, e.Name()) newPath := filepath.Join(dataDir, subdir, e.Name()) data, err := os.ReadFile(oldPath) if err != nil { return nil, fmt.Errorf("read %s: %w", e.Name(), err) } if err := os.WriteFile(newPath, data, 0644); err != nil { return nil, fmt.Errorf("write %s: %w", e.Name(), err) } if err := os.Remove(oldPath); err != nil { return nil, fmt.Errorf("remove %s: %w", e.Name(), err) } changes = append(changes, ChangeDesc{ Description: fmt.Sprintf("move %s %s to root", subdir, e.Name()), FilePath: oldPath, NewFilePath: newPath, OldContent: data, NewContent: data, }) } if err := os.Remove(srcDir); err != nil { return nil, fmt.Errorf("remove directory: %w", err) } changes = append(changes, ChangeDesc{ Description: fmt.Sprintf("delete directory %s/%s", subdir, dir), FilePath: srcDir, IsDelete: true, }) return changes, nil }