diff --git a/README.md b/README.md index 5241cc6..207de33 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ go build -o samba-configer . The program: - Reads the Samba config and parses non-`[global]` sections as shares. +- Detects a missing Samba installation when the config file does not exist yet. +- Offers to install the Samba server package with a detected package manager such as `apt`, `dnf`, `yum`, `pacman`, `zypper`, or `apk`. - Prompts to add, edit, or delete a share. - Checks `valid users` entries against local accounts. - Offers to create missing local accounts with `useradd -M -s /usr/sbin/nologin `. diff --git a/app.go b/app.go index bf8a27b..e5460dc 100644 --- a/app.go +++ b/app.go @@ -24,6 +24,8 @@ type CommandRunner interface { Run(name string, args ...string) error } +type LookPathFunc func(file string) (string, error) + type RealUserManager struct{} func (RealUserManager) UserExists(name string) bool { @@ -49,14 +51,18 @@ func (OSCommandRunner) Run(name string, args ...string) error { type App struct { configPath string users UserManager + runner CommandRunner + lookPath LookPathFunc reader *bufio.Reader writer *bufio.Writer } -func NewApp(configPath string, users UserManager) *App { +func NewApp(configPath string, users UserManager, runner CommandRunner, lookPath LookPathFunc) *App { return &App{ configPath: configPath, users: users, + runner: runner, + lookPath: lookPath, reader: bufio.NewReader(os.Stdin), writer: bufio.NewWriter(os.Stdout), } @@ -65,9 +71,17 @@ func NewApp(configPath string, users UserManager) *App { func (a *App) Run() error { doc, err := ParseConfigFile(a.configPath) if err != nil { + if os.IsNotExist(err) { + doc, err = a.handleMissingConfig() + if err == nil { + goto loaded + } + } + return fmt.Errorf("read config: %w", err) } +loaded: for { shareSections := doc.ShareSections() a.println("") @@ -118,6 +132,47 @@ func (a *App) Run() error { } } +func (a *App) handleMissingConfig() (*Document, error) { + a.println("") + a.println("Samba is not set up yet on this computer.") + a.printf("I couldn't find the Samba config file at %s.\n", a.configPath) + + plan, ok := DetectSambaInstallPlan(a.lookPath, os.Geteuid() == 0) + if !ok { + a.println("I also couldn't find a supported package manager automatically.") + a.println("Install the Samba server package for your Linux distribution, then run this tool again.") + a.flush() + return nil, os.ErrNotExist + } + + a.println("") + a.printf("I found %s and can try to install the Samba server for you.\n", plan.ManagerName) + a.printf("This would run: %s\n", plan.DisplayCommand()) + a.println("You may be asked for your administrator password.") + a.flush() + + install, err := a.confirm("Install Samba server now", true) + if err != nil { + return nil, err + } + if !install { + return nil, os.ErrNotExist + } + + if err := a.runner.Run(plan.Command[0], plan.Command[1:]...); err != nil { + return nil, fmt.Errorf("install Samba with %s: %w", plan.ManagerName, err) + } + + doc, err := ParseConfigFile(a.configPath) + if err != nil { + return nil, fmt.Errorf("Samba installation finished, but the config file is still missing at %s: %w", a.configPath, err) + } + + a.println("Samba was installed and the config file is now available.") + a.flush() + return doc, nil +} + func (a *App) addShare(doc *Document) error { cfg, err := a.collectShareConfig(ShareConfig{}) if err != nil { diff --git a/install.go b/install.go new file mode 100644 index 0000000..d36917c --- /dev/null +++ b/install.go @@ -0,0 +1,63 @@ +package main + +import ( + "os/exec" + "strings" +) + +type InstallPlan struct { + ManagerName string + Command []string +} + +func (p InstallPlan) DisplayCommand() string { + return strings.Join(p.Command, " ") +} + +func execLookPath(file string) (string, error) { + return exec.LookPath(file) +} + +func DetectSambaInstallPlan(lookPath LookPathFunc, isRoot bool) (InstallPlan, bool) { + sudoPrefix := []string{} + if !isRoot { + if _, err := lookPath("sudo"); err == nil { + sudoPrefix = []string{"sudo"} + } + } + + plans := []struct { + bin string + name string + command []string + }{ + {bin: "apt-get", name: "apt", command: []string{"apt-get", "update"}}, + {bin: "apt-get", name: "apt", command: []string{"apt-get", "install", "-y", "samba"}}, + {bin: "dnf", name: "dnf", command: []string{"dnf", "install", "-y", "samba"}}, + {bin: "yum", name: "yum", command: []string{"yum", "install", "-y", "samba"}}, + {bin: "pacman", name: "pacman", command: []string{"pacman", "-Sy", "--noconfirm", "samba"}}, + {bin: "zypper", name: "zypper", command: []string{"zypper", "--non-interactive", "install", "samba"}}, + {bin: "apk", name: "apk", command: []string{"apk", "add", "samba"}}, + } + + if _, err := lookPath("apt-get"); err == nil { + return InstallPlan{ + ManagerName: "apt", + Command: append( + append([]string{}, sudoPrefix...), + "sh", "-c", "apt-get update && apt-get install -y samba", + ), + }, true + } + + for _, plan := range plans[2:] { + if _, err := lookPath(plan.bin); err == nil { + return InstallPlan{ + ManagerName: plan.name, + Command: append(append([]string{}, sudoPrefix...), plan.command...), + }, true + } + } + + return InstallPlan{}, false +} diff --git a/main.go b/main.go index 4916c3b..04b05a2 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ func main() { configPath := flag.String("config", "/etc/samba/smb.conf", "path to smb.conf") flag.Parse() - app := NewApp(*configPath, RealUserManager{}) + app := NewApp(*configPath, RealUserManager{}, OSCommandRunner{}, execLookPath) if err := app.Run(); err != nil { if errors.Is(err, ErrCancelled) { fmt.Fprintln(os.Stderr, "cancelled") diff --git a/parser_test.go b/parser_test.go index 13cf2ff..6b2c2fa 100644 --- a/parser_test.go +++ b/parser_test.go @@ -94,3 +94,50 @@ func TestSerializeProducesSectionHeaders(t *testing.T) { } } } + +func TestDetectSambaInstallPlanPrefersApt(t *testing.T) { + lookPath := func(file string) (string, error) { + switch file { + case "apt-get", "sudo": + return "/usr/bin/" + file, nil + default: + return "", execErr(file) + } + } + + plan, ok := DetectSambaInstallPlan(lookPath, false) + if !ok { + t.Fatalf("expected install plan") + } + if plan.ManagerName != "apt" { + t.Fatalf("expected apt manager, got %q", plan.ManagerName) + } + if plan.DisplayCommand() != "sudo sh -c apt-get update && apt-get install -y samba" { + t.Fatalf("unexpected command: %q", plan.DisplayCommand()) + } +} + +func TestDetectSambaInstallPlanWithoutSudo(t *testing.T) { + lookPath := func(file string) (string, error) { + switch file { + case "pacman": + return "/usr/bin/pacman", nil + default: + return "", execErr(file) + } + } + + plan, ok := DetectSambaInstallPlan(lookPath, false) + if !ok { + t.Fatalf("expected install plan") + } + if plan.DisplayCommand() != "pacman -Sy --noconfirm samba" { + t.Fatalf("unexpected command: %q", plan.DisplayCommand()) + } +} + +type execErr string + +func (e execErr) Error() string { + return "not found: " + string(e) +} diff --git a/samba-configer b/samba-configer index 73d852c..7dbed9f 100755 Binary files a/samba-configer and b/samba-configer differ