diff --git a/README.md b/README.md index f03a6eb..be02a5d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The program: - Offers to create missing local accounts with `useradd -M -s /usr/sbin/nologin `. - If user creation fails because admin rights are needed, explains the issue and offers to retry with `sudo`. - After creating a user, offers to launch `smbpasswd -a ` immediately so the Samba password can be set right away. +- Before saving, checks whether each share folder exists and offers to create missing directories. - If saving the Samba config or its backup needs admin rights, explains the issue and offers to retry with `sudo`. - Writes a timestamped backup before saving changes. diff --git a/app.go b/app.go index 9fa2797..ca52c44 100644 --- a/app.go +++ b/app.go @@ -237,6 +237,10 @@ func (a *App) deleteShare(doc *Document) error { } func (a *App) writeConfig(doc *Document) error { + if err := a.ensureShareDirectories(doc); err != nil { + return err + } + backup := fmt.Sprintf("%s.bak.%s", a.configPath, time.Now().UTC().Format("20060102T150405Z")) serialized := doc.Serialize() @@ -259,6 +263,49 @@ func (a *App) writeConfig(doc *Document) error { return nil } +func (a *App) ensureShareDirectories(doc *Document) error { + for _, section := range doc.ShareSections() { + cfg := ShareFromSection(section) + if strings.TrimSpace(cfg.Path) == "" { + continue + } + + info, err := os.Stat(cfg.Path) + if err == nil { + if info.IsDir() { + continue + } + return fmt.Errorf("share %q path exists but is not a directory: %s", cfg.Name, cfg.Path) + } + + if !os.IsNotExist(err) { + return fmt.Errorf("check share %q path %s: %w", cfg.Name, cfg.Path, err) + } + + a.println("") + a.printf("The folder for share %q does not exist yet.\n", cfg.Name) + a.printf("Missing folder: %s\n", cfg.Path) + a.println("I can create it now so the share is ready to use.") + a.flush() + + create, promptErr := a.confirm("Create this folder now", true) + if promptErr != nil { + return promptErr + } + if !create { + return fmt.Errorf("share %q needs an existing folder before saving", cfg.Name) + } + + if err := a.createDirectory(cfg.Path); err != nil { + return fmt.Errorf("create folder for share %q: %w", cfg.Name, err) + } + + a.printf("Created folder %s\n", cfg.Path) + } + + return nil +} + func (a *App) selectShare(doc *Document) (*Section, error) { shares := doc.ShareSections() if len(shares) == 0 { @@ -608,6 +655,44 @@ func (a *App) writeTempConfig(serialized string) (string, error) { return file.Name(), nil } +func (a *App) createDirectory(path string) error { + if err := os.MkdirAll(path, 0o755); err == nil { + return nil + } else if shouldOfferPrivilegeRetry(err) { + a.println("") + a.println("Creating that folder needs administrator permission.") + a.println("I can try again using sudo so you can enter your admin password.") + a.flush() + + canUseSudo := false + if a.lookPath != nil { + _, sudoErr := a.lookPath("sudo") + canUseSudo = sudoErr == nil + } + if !canUseSudo { + return fmt.Errorf("create folder %s: %w", path, err) + } + + retry, promptErr := a.confirm("Retry folder creation with sudo", true) + if promptErr != nil { + return promptErr + } + if !retry { + return fmt.Errorf("create folder %s: %w", path, err) + } + + if sudoErr := a.runner.Run("sudo", "mkdir", "-p", path); sudoErr != nil { + return fmt.Errorf("create folder %s with sudo: %w", path, sudoErr) + } + if sudoErr := a.runner.Run("sudo", "chmod", "755", path); sudoErr != nil { + return fmt.Errorf("set permissions on folder %s with sudo: %w", path, sudoErr) + } + return nil + } else { + return fmt.Errorf("create folder %s: %w", path, err) + } +} + func friendlyWriteError(action, path string, err error) error { if errors.Is(err, os.ErrPermission) || strings.Contains(strings.ToLower(err.Error()), "permission denied") { return fmt.Errorf("%s %s: administrator permission is needed", action, path) diff --git a/samba-configer b/samba-configer index bd21cd5..8cf4adf 100755 Binary files a/samba-configer and b/samba-configer differ