package shares import ( "context" "database/sql" "encoding/json" "errors" "fmt" "github.com/google/uuid" "strings" "github.com/example/storage-appliance/internal/audit" "github.com/example/storage-appliance/internal/domain" "github.com/example/storage-appliance/internal/infra/nfs" "github.com/example/storage-appliance/internal/infra/samba" ) var ( ErrForbidden = errors.New("forbidden") ) type SharesService struct { DB *sql.DB NFS *nfs.Adapter Samba *samba.Adapter Audit audit.AuditLogger } func NewSharesService(db *sql.DB, n *nfs.Adapter, s *samba.Adapter, a audit.AuditLogger) *SharesService { return &SharesService{DB: db, NFS: n, Samba: s, Audit: a} } func (s *SharesService) ListNFS(ctx context.Context) ([]domain.Share, error) { rows, err := s.DB.QueryContext(ctx, `SELECT id, name, path, type, options FROM shares WHERE type = 'nfs'`) if err != nil { return nil, err } defer rows.Close() var res []domain.Share for rows.Next() { var id, name, path, typ, options string if err := rows.Scan(&id, &name, &path, &typ, &options); err != nil { return nil, err } var optMap map[string]string if options != "" { _ = json.Unmarshal([]byte(options), &optMap) } res = append(res, domain.Share{ID: domain.UUID(id), Name: name, Path: path, Type: typ}) } return res, nil } // CreateNFS stores a new NFS export, re-renders /etc/exports and applies it func (s *SharesService) CreateNFS(ctx context.Context, user, role, name, path string, opts map[string]string) (string, error) { if role != "admin" && role != "operator" { return "", ErrForbidden } // Verify path exists and is a dataset: check dataset table for matching name var count int if err := s.DB.QueryRowContext(ctx, `SELECT COUNT(1) FROM datasets WHERE name = ?`, path).Scan(&count); err != nil { return "", err } if count == 0 { return "", fmt.Errorf("path not a known dataset: %s", path) } // Prevent exporting system paths: disallow leading '/' entries; require dataset name like pool/ds if path == "/" || path == "/etc" || path == "/bin" || path == "/usr" { return "", fmt.Errorf("can't export system path: %s", path) } // store options as JSON optJSON, _ := json.Marshal(opts) id := uuid.New().String() if _, err := s.DB.ExecContext(ctx, `INSERT INTO shares (id, name, path, type, options) VALUES (?, ?, ?, 'nfs', ?)`, id, name, path, string(optJSON)); err != nil { return "", err } if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "nfs.create", ResourceType: "share", ResourceID: name, Success: true, Details: map[string]any{"path": path}}) } // re-render exports shares, err := s.ListNFS(ctx) if err != nil { return id, err } if s.NFS != nil { if err := s.NFS.RenderExports(ctx, shares); err != nil { return id, err } if err := s.NFS.Apply(ctx); err != nil { return id, err } } return id, nil } // SMB functions func (s *SharesService) ListSMB(ctx context.Context) ([]domain.Share, error) { rows, err := s.DB.QueryContext(ctx, `SELECT id, name, path, type, options FROM shares WHERE type = 'smb'`) if err != nil { return nil, err } defer rows.Close() var res []domain.Share for rows.Next() { var id, name, path, typ, options string if err := rows.Scan(&id, &name, &path, &typ, &options); err != nil { return nil, err } var config map[string]string if options != "" { _ = json.Unmarshal([]byte(options), &config) } res = append(res, domain.Share{ID: domain.UUID(id), Name: name, Path: path, Type: typ, Config: config}) } return res, nil } func (s *SharesService) CreateSMB(ctx context.Context, user, role, name, path string, readOnly bool, allowedUsers []string) (string, error) { if role != "admin" && role != "operator" { return "", ErrForbidden } // Verify dataset var count int if err := s.DB.QueryRowContext(ctx, `SELECT COUNT(1) FROM datasets WHERE name = ?`, path).Scan(&count); err != nil { return "", err } if count == 0 { return "", fmt.Errorf("path not a known dataset: %s", path) } // disallow system paths by basic checks if path == "/" || path == "/etc" || path == "/bin" || path == "/usr" { return "", fmt.Errorf("can't export system path: %s", path) } // store options as JSON (read_only, allowed_users) cfg := map[string]string{} if readOnly { cfg["read_only"] = "true" } else { cfg["read_only"] = "false" } if len(allowedUsers) > 0 { cfg["allowed_users"] = strings.Join(allowedUsers, " ") } optJSON, _ := json.Marshal(cfg) id := uuid.New().String() if _, err := s.DB.ExecContext(ctx, `INSERT INTO shares (id, name, path, type, options) VALUES (?, ?, ?, 'smb', ?)`, id, name, path, string(optJSON)); err != nil { return "", err } if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "smb.create", ResourceType: "share", ResourceID: name, Success: true, Details: map[string]any{"path": path, "read_only": readOnly}}) } // re-render smb conf and reload shares, err := s.ListSMB(ctx) if err != nil { return id, err } if s.Samba != nil { if err := s.Samba.RenderConf(ctx, shares); err != nil { return id, err } if err := s.Samba.Reload(ctx); err != nil { return id, err } } return id, nil } func (s *SharesService) DeleteSMB(ctx context.Context, user, role, id string) error { if role != "admin" && role != "operator" { return ErrForbidden } if _, err := s.DB.ExecContext(ctx, `DELETE FROM shares WHERE id = ?`, id); err != nil { return err } if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "smb.delete", ResourceType: "share", ResourceID: id, Success: true}) } shares, err := s.ListSMB(ctx) if err != nil { return err } if s.Samba != nil { if err := s.Samba.RenderConf(ctx, shares); err != nil { return err } if err := s.Samba.Reload(ctx); err != nil { return err } } return nil } func (s *SharesService) DeleteNFS(ctx context.Context, user, role, id string) error { if role != "admin" && role != "operator" { return ErrForbidden } // verify exists if _, err := s.DB.ExecContext(ctx, `DELETE FROM shares WHERE id = ?`, id); err != nil { return err } if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "nfs.delete", ResourceType: "share", ResourceID: id, Success: true}) } // re-render exports shares, err := s.ListNFS(ctx) if err != nil { return err } if s.NFS != nil { if err := s.NFS.RenderExports(ctx, shares); err != nil { return err } if err := s.NFS.Apply(ctx); err != nil { return err } } return nil } func (s *SharesService) NFSStatus(ctx context.Context) (string, error) { if s.NFS == nil { return "unavailable", nil } return s.NFS.Status(ctx) }