This commit is contained in:
2026-03-19 15:39:31 +00:00
parent b7987248c1
commit cd7d2fcf61
6 changed files with 169 additions and 2 deletions

View File

@@ -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 <user>`.

57
app.go
View File

@@ -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 {

63
install.go Normal file
View File

@@ -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
}

View File

@@ -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")

View File

@@ -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)
}

Binary file not shown.