init
This commit is contained in:
31
README.md
Normal file
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# samba-configer
|
||||
|
||||
`samba-configer` is a small interactive terminal program for reading an existing Samba configuration, listing shares, and guiding add/edit/delete operations.
|
||||
|
||||
It is implemented in Go with only the standard library so it builds into a single executable easily.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
go build -o samba-configer .
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
./samba-configer -config /etc/samba/smb.conf
|
||||
```
|
||||
|
||||
The program:
|
||||
|
||||
- Reads the Samba config and parses non-`[global]` sections as shares.
|
||||
- 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>`.
|
||||
- Writes a timestamped backup before saving changes.
|
||||
|
||||
If you create a local account for a Samba-authenticated share, you may still need to add the Samba password separately:
|
||||
|
||||
```bash
|
||||
smbpasswd -a <username>
|
||||
```
|
||||
384
app.go
Normal file
384
app.go
Normal file
@@ -0,0 +1,384 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrCancelled = errors.New("cancelled")
|
||||
|
||||
type UserManager interface {
|
||||
UserExists(name string) bool
|
||||
CreateUser(name string) error
|
||||
}
|
||||
|
||||
type CommandRunner interface {
|
||||
Run(name string, args ...string) error
|
||||
}
|
||||
|
||||
type RealUserManager struct{}
|
||||
|
||||
func (RealUserManager) UserExists(name string) bool {
|
||||
_, err := user.Lookup(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (RealUserManager) CreateUser(name string) error {
|
||||
runner := OSCommandRunner{}
|
||||
return runner.Run("useradd", "-M", "-s", "/usr/sbin/nologin", name)
|
||||
}
|
||||
|
||||
type OSCommandRunner struct{}
|
||||
|
||||
func (OSCommandRunner) Run(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
type App struct {
|
||||
configPath string
|
||||
users UserManager
|
||||
reader *bufio.Reader
|
||||
writer *bufio.Writer
|
||||
}
|
||||
|
||||
func NewApp(configPath string, users UserManager) *App {
|
||||
return &App{
|
||||
configPath: configPath,
|
||||
users: users,
|
||||
reader: bufio.NewReader(os.Stdin),
|
||||
writer: bufio.NewWriter(os.Stdout),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Run() error {
|
||||
doc, err := ParseConfigFile(a.configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read config: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
shareSections := doc.ShareSections()
|
||||
a.println("")
|
||||
a.println("Samba share editor")
|
||||
a.println("==================")
|
||||
a.println("Current shares:")
|
||||
if len(shareSections) == 0 {
|
||||
a.println(" (none)")
|
||||
} else {
|
||||
for i, section := range shareSections {
|
||||
cfg := ShareFromSection(section)
|
||||
a.printf(" %d. %s -> %s\n", i+1, cfg.Name, cfg.Path)
|
||||
}
|
||||
}
|
||||
a.println("")
|
||||
a.println("[a] add share")
|
||||
a.println("[e] edit share")
|
||||
a.println("[d] delete share")
|
||||
a.println("[w] write config and exit")
|
||||
a.println("[q] quit without saving")
|
||||
a.flush()
|
||||
|
||||
choice, err := a.prompt("Select an action")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch strings.ToLower(choice) {
|
||||
case "a", "add":
|
||||
if err := a.addShare(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
case "e", "edit":
|
||||
if err := a.editShare(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
case "d", "delete":
|
||||
if err := a.deleteShare(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
case "w", "write":
|
||||
return a.writeConfig(doc)
|
||||
case "q", "quit":
|
||||
return nil
|
||||
default:
|
||||
a.println("Unknown choice.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) addShare(doc *Document) error {
|
||||
cfg, err := a.collectShareConfig(ShareConfig{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if doc.Section(cfg.Name) != nil {
|
||||
a.println("A share with that name already exists.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := a.ensureUsers(cfg.ValidUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doc.UpsertSection(BuildShareSection(nil, cfg))
|
||||
a.println("Share added.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) editShare(doc *Document) error {
|
||||
section, err := a.selectShare(doc)
|
||||
if err != nil || section == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
originalName := section.Name
|
||||
cfg, err := a.collectShareConfig(ShareFromSection(section))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.EqualFold(originalName, cfg.Name) && doc.Section(cfg.Name) != nil {
|
||||
a.println("A share with that new name already exists.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := a.ensureUsers(cfg.ValidUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.EqualFold(originalName, cfg.Name) {
|
||||
doc.DeleteSection(originalName)
|
||||
}
|
||||
doc.UpsertSection(BuildShareSection(section, cfg))
|
||||
a.println("Share updated.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) deleteShare(doc *Document) error {
|
||||
section, err := a.selectShare(doc)
|
||||
if err != nil || section == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confirm, err := a.confirm(fmt.Sprintf("Delete share %q", section.Name), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirm {
|
||||
a.println("Delete cancelled.")
|
||||
return nil
|
||||
}
|
||||
|
||||
doc.DeleteSection(section.Name)
|
||||
a.println("Share deleted.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) writeConfig(doc *Document) error {
|
||||
backup := fmt.Sprintf("%s.bak.%s", a.configPath, time.Now().UTC().Format("20060102T150405Z"))
|
||||
if err := copyFile(a.configPath, backup); err != nil {
|
||||
return fmt.Errorf("create backup %s: %w", backup, err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(a.configPath, []byte(doc.Serialize()), 0o644); err != nil {
|
||||
return fmt.Errorf("write config: %w", err)
|
||||
}
|
||||
|
||||
a.printf("Config written to %s\n", a.configPath)
|
||||
a.printf("Backup saved to %s\n", backup)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) selectShare(doc *Document) (*Section, error) {
|
||||
shares := doc.ShareSections()
|
||||
if len(shares) == 0 {
|
||||
a.println("There are no shares to select.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
raw, err := a.prompt("Share number")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index, err := strconv.Atoi(raw)
|
||||
if err != nil || index < 1 || index > len(shares) {
|
||||
a.println("Invalid selection.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return shares[index-1], nil
|
||||
}
|
||||
|
||||
func (a *App) collectShareConfig(existing ShareConfig) (ShareConfig, error) {
|
||||
name, err := a.promptDefault("Share name", existing.Name)
|
||||
if err != nil {
|
||||
return ShareConfig{}, err
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return ShareConfig{}, errors.New("share name cannot be empty")
|
||||
}
|
||||
|
||||
path, err := a.promptDefault("Path", existing.Path)
|
||||
if err != nil {
|
||||
return ShareConfig{}, err
|
||||
}
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return ShareConfig{}, errors.New("path cannot be empty")
|
||||
}
|
||||
|
||||
comment, err := a.promptDefault("Comment", existing.Comment)
|
||||
if err != nil {
|
||||
return ShareConfig{}, err
|
||||
}
|
||||
|
||||
browseable, err := a.promptDefault("Browseable (yes/no)", defaultString(existing.Browseable, "yes"))
|
||||
if err != nil {
|
||||
return ShareConfig{}, err
|
||||
}
|
||||
|
||||
readOnly, err := a.promptDefault("Read only (yes/no)", defaultString(existing.ReadOnly, "no"))
|
||||
if err != nil {
|
||||
return ShareConfig{}, err
|
||||
}
|
||||
|
||||
guestOK, err := a.promptDefault("Guest ok (yes/no)", defaultString(existing.GuestOK, "no"))
|
||||
if err != nil {
|
||||
return ShareConfig{}, err
|
||||
}
|
||||
|
||||
usersDefault := strings.Join(existing.ValidUsers, ", ")
|
||||
usersRaw, err := a.promptDefault("Valid users (comma or space separated, blank for none)", usersDefault)
|
||||
if err != nil {
|
||||
return ShareConfig{}, err
|
||||
}
|
||||
|
||||
return ShareConfig{
|
||||
Name: name,
|
||||
Path: filepath.Clean(path),
|
||||
Comment: strings.TrimSpace(comment),
|
||||
Browseable: browseable,
|
||||
ReadOnly: readOnly,
|
||||
GuestOK: guestOK,
|
||||
ValidUsers: splitUsers(usersRaw),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *App) ensureUsers(users []string) error {
|
||||
for _, name := range users {
|
||||
if a.users.UserExists(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
create, err := a.confirm(fmt.Sprintf("User %q does not exist. Create it with useradd", name), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !create {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := a.users.CreateUser(name); err != nil {
|
||||
return fmt.Errorf("create user %s: %w", name, err)
|
||||
}
|
||||
|
||||
a.printf("Created local user %s\n", name)
|
||||
a.println("If Samba authentication is required, add a Samba password with: smbpasswd -a " + name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) prompt(label string) (string, error) {
|
||||
a.printf("%s: ", label)
|
||||
a.flush()
|
||||
text, err := a.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(text), nil
|
||||
}
|
||||
|
||||
func (a *App) promptDefault(label, defaultValue string) (string, error) {
|
||||
if defaultValue == "" {
|
||||
return a.prompt(label)
|
||||
}
|
||||
|
||||
a.printf("%s [%s]: ", label, defaultValue)
|
||||
a.flush()
|
||||
text, err := a.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func (a *App) confirm(label string, defaultYes bool) (bool, error) {
|
||||
suffix := "y/N"
|
||||
if defaultYes {
|
||||
suffix = "Y/n"
|
||||
}
|
||||
|
||||
answer, err := a.prompt(fmt.Sprintf("%s [%s]", label, suffix))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if answer == "" {
|
||||
return defaultYes, nil
|
||||
}
|
||||
|
||||
switch strings.ToLower(answer) {
|
||||
case "y", "yes":
|
||||
return true, nil
|
||||
case "n", "no":
|
||||
return false, nil
|
||||
default:
|
||||
return false, errors.New("expected yes or no")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) println(line string) {
|
||||
fmt.Fprintln(a.writer, line)
|
||||
}
|
||||
|
||||
func (a *App) printf(format string, args ...any) {
|
||||
fmt.Fprintf(a.writer, format, args...)
|
||||
}
|
||||
|
||||
func (a *App) flush() {
|
||||
_ = a.writer.Flush()
|
||||
}
|
||||
|
||||
func defaultString(value, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, data, 0o644)
|
||||
}
|
||||
24
main.go
Normal file
24
main.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "/etc/samba/smb.conf", "path to smb.conf")
|
||||
flag.Parse()
|
||||
|
||||
app := NewApp(*configPath, RealUserManager{})
|
||||
if err := app.Run(); err != nil {
|
||||
if errors.Is(err, ErrCancelled) {
|
||||
fmt.Fprintln(os.Stderr, "cancelled")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
297
parser.go
Normal file
297
parser.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Document struct {
|
||||
Preamble []string
|
||||
Sections []*Section
|
||||
}
|
||||
|
||||
type Section struct {
|
||||
Name string
|
||||
Lines []Line
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
Raw string
|
||||
Key string
|
||||
Value string
|
||||
IsKV bool
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
func ParseConfigFile(path string) (*Document, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ParseConfig(string(data))
|
||||
}
|
||||
|
||||
func ParseConfig(contents string) (*Document, error) {
|
||||
doc := &Document{}
|
||||
var current *Section
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(contents))
|
||||
for scanner.Scan() {
|
||||
raw := scanner.Text()
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
name := strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
current = &Section{Name: name}
|
||||
doc.Sections = append(doc.Sections, current)
|
||||
continue
|
||||
}
|
||||
|
||||
line := parseLine(raw)
|
||||
if current == nil {
|
||||
doc.Preamble = append(doc.Preamble, raw)
|
||||
continue
|
||||
}
|
||||
current.Lines = append(current.Lines, line)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func parseLine(raw string) Line {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, ";") {
|
||||
return Line{Raw: raw}
|
||||
}
|
||||
|
||||
disabled := false
|
||||
parseTarget := trimmed
|
||||
if strings.HasPrefix(parseTarget, "#") || strings.HasPrefix(parseTarget, ";") {
|
||||
disabled = true
|
||||
parseTarget = strings.TrimSpace(parseTarget[1:])
|
||||
}
|
||||
|
||||
key, value, ok := strings.Cut(parseTarget, "=")
|
||||
if !ok {
|
||||
return Line{Raw: raw}
|
||||
}
|
||||
|
||||
return Line{
|
||||
Raw: raw,
|
||||
Key: strings.TrimSpace(strings.ToLower(key)),
|
||||
Value: strings.TrimSpace(value),
|
||||
IsKV: true,
|
||||
Disabled: disabled,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Document) ShareSections() []*Section {
|
||||
var shares []*Section
|
||||
for _, section := range d.Sections {
|
||||
if !strings.EqualFold(section.Name, "global") {
|
||||
shares = append(shares, section)
|
||||
}
|
||||
}
|
||||
return shares
|
||||
}
|
||||
|
||||
func (d *Document) Section(name string) *Section {
|
||||
for _, section := range d.Sections {
|
||||
if strings.EqualFold(section.Name, name) {
|
||||
return section
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Document) DeleteSection(name string) bool {
|
||||
for i, section := range d.Sections {
|
||||
if strings.EqualFold(section.Name, name) {
|
||||
d.Sections = append(d.Sections[:i], d.Sections[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *Document) UpsertSection(section *Section) {
|
||||
for i, existing := range d.Sections {
|
||||
if strings.EqualFold(existing.Name, section.Name) {
|
||||
d.Sections[i] = section
|
||||
return
|
||||
}
|
||||
}
|
||||
d.Sections = append(d.Sections, section)
|
||||
}
|
||||
|
||||
func (d *Document) Serialize() string {
|
||||
var b strings.Builder
|
||||
for _, line := range d.Preamble {
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(d.Preamble) > 0 && len(d.Sections) > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
for i, section := range d.Sections {
|
||||
b.WriteString(fmt.Sprintf("[%s]\n", section.Name))
|
||||
for _, line := range section.Lines {
|
||||
if line.IsKV {
|
||||
if line.Disabled {
|
||||
b.WriteString(fmt.Sprintf("# %s = %s\n", line.Key, line.Value))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf(" %s = %s\n", line.Key, line.Value))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
b.WriteString(line.Raw)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if i < len(d.Sections)-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type ShareConfig struct {
|
||||
Name string
|
||||
Path string
|
||||
Comment string
|
||||
Browseable string
|
||||
ReadOnly string
|
||||
GuestOK string
|
||||
ValidUsers []string
|
||||
}
|
||||
|
||||
func ShareFromSection(section *Section) ShareConfig {
|
||||
cfg := ShareConfig{
|
||||
Name: section.Name,
|
||||
Browseable: "yes",
|
||||
ReadOnly: "no",
|
||||
GuestOK: "no",
|
||||
}
|
||||
|
||||
for _, line := range section.Lines {
|
||||
if !line.IsKV || line.Disabled {
|
||||
continue
|
||||
}
|
||||
|
||||
switch line.Key {
|
||||
case "path":
|
||||
cfg.Path = line.Value
|
||||
case "comment":
|
||||
cfg.Comment = line.Value
|
||||
case "browseable", "browsable":
|
||||
cfg.Browseable = line.Value
|
||||
case "read only", "readonly":
|
||||
cfg.ReadOnly = line.Value
|
||||
case "guest ok":
|
||||
cfg.GuestOK = line.Value
|
||||
case "valid users":
|
||||
cfg.ValidUsers = splitUsers(line.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func BuildShareSection(existing *Section, cfg ShareConfig) *Section {
|
||||
var lines []Line
|
||||
if existing != nil {
|
||||
lines = slices.Clone(existing.Lines)
|
||||
} else {
|
||||
lines = []Line{}
|
||||
}
|
||||
|
||||
section := &Section{
|
||||
Name: cfg.Name,
|
||||
Lines: lines,
|
||||
}
|
||||
|
||||
section.SetValue("path", cfg.Path)
|
||||
if cfg.Comment != "" {
|
||||
section.SetValue("comment", cfg.Comment)
|
||||
} else {
|
||||
section.DeleteValue("comment")
|
||||
}
|
||||
section.SetValue("browseable", normalizeBoolish(cfg.Browseable, "yes"))
|
||||
section.SetValue("read only", normalizeBoolish(cfg.ReadOnly, "no"))
|
||||
section.SetValue("guest ok", normalizeBoolish(cfg.GuestOK, "no"))
|
||||
if len(cfg.ValidUsers) > 0 {
|
||||
section.SetValue("valid users", strings.Join(cfg.ValidUsers, ", "))
|
||||
} else {
|
||||
section.DeleteValue("valid users")
|
||||
}
|
||||
|
||||
return section
|
||||
}
|
||||
|
||||
func (s *Section) SetValue(key, value string) {
|
||||
canonical := strings.ToLower(strings.TrimSpace(key))
|
||||
for i, line := range s.Lines {
|
||||
if line.IsKV && line.Key == canonical {
|
||||
s.Lines[i].Value = value
|
||||
s.Lines[i].Disabled = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.Lines = append(s.Lines, Line{
|
||||
Key: canonical,
|
||||
Value: value,
|
||||
IsKV: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Section) DeleteValue(key string) {
|
||||
canonical := strings.ToLower(strings.TrimSpace(key))
|
||||
dst := s.Lines[:0]
|
||||
for _, line := range s.Lines {
|
||||
if line.IsKV && line.Key == canonical {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, line)
|
||||
}
|
||||
s.Lines = dst
|
||||
}
|
||||
|
||||
func splitUsers(raw string) []string {
|
||||
fields := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == '\t'
|
||||
})
|
||||
|
||||
var users []string
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" {
|
||||
users = append(users, field)
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func normalizeBoolish(value, fallback string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "yes", "true", "1":
|
||||
return "yes"
|
||||
case "no", "false", "0":
|
||||
return "no"
|
||||
case "":
|
||||
return fallback
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
96
parser_test.go
Normal file
96
parser_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseConfigFindsShares(t *testing.T) {
|
||||
doc, err := ParseConfig(`
|
||||
# comment
|
||||
[global]
|
||||
workgroup = WORKGROUP
|
||||
|
||||
[media]
|
||||
path = /srv/media
|
||||
valid users = alice, bob
|
||||
guest ok = no
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConfig() error = %v", err)
|
||||
}
|
||||
|
||||
shares := doc.ShareSections()
|
||||
if len(shares) != 1 {
|
||||
t.Fatalf("expected 1 share, got %d", len(shares))
|
||||
}
|
||||
|
||||
cfg := ShareFromSection(shares[0])
|
||||
if cfg.Name != "media" {
|
||||
t.Fatalf("expected share name media, got %q", cfg.Name)
|
||||
}
|
||||
if cfg.Path != "/srv/media" {
|
||||
t.Fatalf("expected path /srv/media, got %q", cfg.Path)
|
||||
}
|
||||
if strings.Join(cfg.ValidUsers, ",") != "alice,bob" {
|
||||
t.Fatalf("expected users alice,bob, got %v", cfg.ValidUsers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildShareSectionUpdatesValues(t *testing.T) {
|
||||
section := &Section{
|
||||
Name: "public",
|
||||
Lines: []Line{
|
||||
{Key: "path", Value: "/old", IsKV: true},
|
||||
{Key: "comment", Value: "old", IsKV: true},
|
||||
},
|
||||
}
|
||||
|
||||
updated := BuildShareSection(section, ShareConfig{
|
||||
Name: "public",
|
||||
Path: "/srv/public",
|
||||
Comment: "",
|
||||
Browseable: "yes",
|
||||
ReadOnly: "no",
|
||||
GuestOK: "yes",
|
||||
ValidUsers: []string{"alice"},
|
||||
})
|
||||
|
||||
cfg := ShareFromSection(updated)
|
||||
if cfg.Path != "/srv/public" {
|
||||
t.Fatalf("expected updated path, got %q", cfg.Path)
|
||||
}
|
||||
if cfg.Comment != "" {
|
||||
t.Fatalf("expected comment removed, got %q", cfg.Comment)
|
||||
}
|
||||
if strings.Join(cfg.ValidUsers, ",") != "alice" {
|
||||
t.Fatalf("expected alice, got %v", cfg.ValidUsers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerializeProducesSectionHeaders(t *testing.T) {
|
||||
doc := &Document{
|
||||
Preamble: []string{"# smb.conf"},
|
||||
Sections: []*Section{
|
||||
BuildShareSection(nil, ShareConfig{
|
||||
Name: "docs",
|
||||
Path: "/srv/docs",
|
||||
Browseable: "yes",
|
||||
ReadOnly: "no",
|
||||
GuestOK: "no",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
out := doc.Serialize()
|
||||
for _, want := range []string{
|
||||
"# smb.conf",
|
||||
"[docs]",
|
||||
"path = /srv/docs",
|
||||
"browseable = yes",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("serialized output missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
samba-configer
Executable file
BIN
samba-configer
Executable file
Binary file not shown.
Reference in New Issue
Block a user