package nfs import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/example/storage-appliance/internal/domain" "github.com/example/storage-appliance/internal/infra/osexec" ) type Adapter struct { Runner osexec.Runner ExportsPath string } func NewAdapter(runner osexec.Runner, exportsPath string) *Adapter { if exportsPath == "" { exportsPath = "/etc/exports" } return &Adapter{Runner: runner, ExportsPath: exportsPath} } // RenderExports renders the given shares into /etc/exports atomically func (a *Adapter) RenderExports(ctx context.Context, shares []domain.Share) error { var lines []string for _, s := range shares { // default options for NFS export opts := "rw,sync,no_root_squash" if s.Type == "nfs" { // if options stored as JSON use it if sPath := s.Path; sPath != "" { // options may be in s.Name? No, for now use default } } lines = append(lines, fmt.Sprintf("%s %s", s.Path, opts)) } content := strings.Join(lines, "\n") + "\n" dir := filepath.Dir(a.ExportsPath) tmp := filepath.Join(dir, ".exports.tmp") if err := os.WriteFile(tmp, []byte(content), 0644); err != nil { return err } // atomic rename if err := os.Rename(tmp, a.ExportsPath); err != nil { return err } return nil } // Apply runs exportfs -ra to apply exports func (a *Adapter) Apply(ctx context.Context) error { _, stderr, _, err := osexec.ExecWithRunner(a.Runner, ctx, "exportfs", "-ra") if err != nil { return fmt.Errorf("exportfs failed: %s", stderr) } return nil } // Status checks systemd for nfs server status func (a *Adapter) Status(ctx context.Context) (string, error) { // try common unit names names := []string{"nfs-server", "nfs-kernel-server"} for _, n := range names { out, _, _, err := osexec.ExecWithRunner(a.Runner, ctx, "systemctl", "is-active", n) if err == nil && strings.TrimSpace(out) != "" { return strings.TrimSpace(out), nil } } return "unknown", nil }