package services import ( "fmt" "os" "os/exec" "strings" "sync" "gitea.avt.data-center.id/othman.suseno/atlas/internal/models" ) // SMBService manages Samba service integration type SMBService struct { mu sync.RWMutex configPath string smbConfPath string smbctlPath string } // NewSMBService creates a new SMB service manager func NewSMBService() *SMBService { return &SMBService{ configPath: "/etc/samba/smb.conf", smbctlPath: "smbcontrol", } } // ApplyConfiguration generates and applies SMB configuration func (s *SMBService) ApplyConfiguration(shares []models.SMBShare) error { s.mu.Lock() defer s.mu.Unlock() config, err := s.generateConfig(shares) if err != nil { return fmt.Errorf("generate config: %w", err) } // Write configuration to a temporary file first tmpPath := s.configPath + ".atlas.tmp" if err := os.WriteFile(tmpPath, []byte(config), 0644); err != nil { return fmt.Errorf("write config: %w", err) } // Backup existing config backupPath := s.configPath + ".backup" if _, err := os.Stat(s.configPath); err == nil { if err := exec.Command("cp", s.configPath, backupPath).Run(); err != nil { // Non-fatal, log but continue } } // Atomically replace config if err := os.Rename(tmpPath, s.configPath); err != nil { return fmt.Errorf("replace config: %w", err) } // Reload Samba service if err := s.reloadService(); err != nil { // Try to restore backup on failure if _, err2 := os.Stat(backupPath); err2 == nil { os.Rename(backupPath, s.configPath) } return fmt.Errorf("reload service: %w", err) } return nil } // generateConfig generates Samba configuration from shares func (s *SMBService) generateConfig(shares []models.SMBShare) (string, error) { var b strings.Builder // Global section b.WriteString("[global]\n") b.WriteString(" workgroup = WORKGROUP\n") b.WriteString(" server string = AtlasOS Storage Server\n") b.WriteString(" security = user\n") b.WriteString(" map to guest = Bad User\n") b.WriteString(" dns proxy = no\n") b.WriteString("\n") // Share sections for _, share := range shares { if !share.Enabled { continue } b.WriteString(fmt.Sprintf("[%s]\n", share.Name)) b.WriteString(fmt.Sprintf(" path = %s\n", share.Path)) if share.Description != "" { b.WriteString(fmt.Sprintf(" comment = %s\n", share.Description)) } if share.ReadOnly { b.WriteString(" read only = yes\n") } else { b.WriteString(" read only = no\n") b.WriteString(" writable = yes\n") } if share.GuestOK { b.WriteString(" guest ok = yes\n") b.WriteString(" public = yes\n") } else { b.WriteString(" guest ok = no\n") } if len(share.ValidUsers) > 0 { b.WriteString(fmt.Sprintf(" valid users = %s\n", strings.Join(share.ValidUsers, ", "))) } b.WriteString(" browseable = yes\n") b.WriteString("\n") } return b.String(), nil } // reloadService reloads Samba configuration func (s *SMBService) reloadService() error { // Try smbcontrol first (doesn't require root for reload) cmd := exec.Command(s.smbctlPath, "all", "reload-config") if err := cmd.Run(); err == nil { return nil } // Fallback to systemctl if available cmd = exec.Command("systemctl", "reload", "smbd") if err := cmd.Run(); err == nil { return nil } // Try service command cmd = exec.Command("service", "smbd", "reload") if err := cmd.Run(); err == nil { return nil } return fmt.Errorf("unable to reload Samba service") } // ValidateConfiguration validates SMB configuration syntax func (s *SMBService) ValidateConfiguration(config string) error { // Use testparm to validate configuration cmd := exec.Command("testparm", "-s") cmd.Stdin = strings.NewReader(config) if err := cmd.Run(); err != nil { return fmt.Errorf("configuration validation failed: %w", err) } return nil } // GetStatus returns the status of Samba service func (s *SMBService) GetStatus() (bool, error) { // Check if smbd is running cmd := exec.Command("systemctl", "is-active", "smbd") if err := cmd.Run(); err == nil { return true, nil } // Fallback: check process cmd = exec.Command("pgrep", "-x", "smbd") if err := cmd.Run(); err == nil { return true, nil } return false, nil }