package importer import ( "bufio" "bytes" "encoding/json" "fmt" "os" "regexp" "strings" "github.com/darroyo/nasctl/internal/db" "github.com/darroyo/nasctl/internal/system" ) var nfsClientPattern = regexp.MustCompile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$`) func ImportNFSExports(path string) ([]db.NFSExport, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("read exports: %w", err) } return parseExports(data) } func parseExports(data []byte) ([]db.NFSExport, error) { var exports []db.NFSExport scanner := bufio.NewScanner(bytes.NewReader(data)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } export, ok := parseExportLine(line) if !ok { continue } if err := system.ValidatePath(export.Path); err != nil { continue } validClients := make([]db.NFSClient, 0, len(export.Clients)) for _, c := range export.Clients { if err := system.ValidateNFSClient(c.Host); err != nil { continue } validClients = append(validClients, c) } if len(validClients) == 0 && len(export.Clients) > 0 { continue } export.Clients = validClients fsid, err := generateImportFSID() if err != nil { continue } export.FSID = fsid exports = append(exports, export) } return exports, nil } func parseExportLine(line string) (db.NFSExport, bool) { parenDepth := 0 spaceIdx := -1 for i, ch := range line { switch ch { case '(': parenDepth++ case ')': parenDepth-- case ' ': if parenDepth == 0 && spaceIdx < 0 { spaceIdx = i } } } if spaceIdx < 0 { return db.NFSExport{}, false } path := strings.TrimSpace(line[:spaceIdx]) if path == "" { return db.NFSExport{}, false } rest := strings.TrimSpace(line[spaceIdx:]) if rest == "" { return db.NFSExport{}, false } var clients []db.NFSClient var firstOpts string parts := splitExportsClients(rest) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } open := strings.IndexByte(part, '(') close := strings.LastIndexByte(part, ')') var host, opts string if open >= 0 && close > open { host = strings.TrimSpace(part[:open]) opts = strings.TrimSpace(part[open+1 : close]) } else { host = part opts = "" } if !nfsClientPattern.MatchString(host) { continue } if opts == "" { opts = firstOpts if opts == "" { opts = "rw,sync,no_root_squash" } } else if firstOpts == "" { firstOpts = opts } readOnly := strings.Contains(opts, "ro") async := strings.Contains(opts, "async") rootSquash := !strings.Contains(opts, "no_root_squash") subtreeCheck := strings.Contains(opts, "subtree_check") adv := db.NFSAdvanced{ AllSquash: strings.Contains(opts, "all_squash"), Secure: strings.Contains(opts, "secure"), WDelay: strings.Contains(opts, "wdelay"), Hide: strings.Contains(opts, "hide"), Crossmnt: strings.Contains(opts, "crossmnt"), } clients = append(clients, db.NFSClient{ Host: host, ReadOnly: readOnly, Async: async, RootSquash: rootSquash, SubtreeCheck: subtreeCheck, Advanced: adv, }) } if len(clients) == 0 { return db.NFSExport{}, false } first := clients[0] advJSON, _ := json.Marshal(first.Advanced) return db.NFSExport{ Path: path, Clients: clients, ReadOnly: first.ReadOnly, Async: first.Async, RootSquash: first.RootSquash, SubtreeCheck: first.SubtreeCheck, Advanced: string(advJSON), }, true } func generateImportFSID() (int64, error) { n, err := system.GenerateRandomFSID() if err != nil { return 0, err } return int64(n), nil } func splitExportsClients(s string) []string { var result []string var current []byte parenDepth := 0 for _, ch := range []byte(s) { switch ch { case '(': parenDepth++ current = append(current, ch) case ')': parenDepth-- current = append(current, ch) case ' ': if parenDepth == 0 { if len(current) > 0 { result = append(result, string(current)) current = nil } continue } current = append(current, ch) default: current = append(current, ch) } } if len(current) > 0 { result = append(result, string(current)) } return result }