Files
nasmon/internal/shares/render.go
T
a.gritsenkoandClaude Opus 5.5 6362f1b9f7
build / build (push) Successful in 1m2s
SMB: map to guest = Never when no share allows guests
With "Bad User" an unknown account (the Windows login) silently became a
guest session; Windows 10/11 refuse guest sessions and report "network
path not found" instead of asking for a password, so password-only shares
were unreachable from Explorer. Map to guest only while a guest share
exists, and document the net use workaround on the shares page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:50:12 +03:00

131 lines
3.4 KiB
Go

package shares
import (
"fmt"
"strings"
)
// Header marks generated files so install scripts and humans can tell them apart.
const Header = "# Managed by nasmon — edits are overwritten. Change shares at http://<nas>:8090/shares"
func yesno(b bool) string {
if b {
return "yes"
}
return "no"
}
func anyGuest(c Config) bool {
for _, s := range c.SMB.Shares {
if s.Guest {
return true
}
}
return false
}
// RenderSMB produces the complete smb.conf. owner is the Unix account every
// SMB session acts as (force user), so files end up with the same owner the
// containers use.
func RenderSMB(c Config, owner string) string {
var b strings.Builder
fmt.Fprintf(&b, "%s\n\n[global]\n", Header)
// Bad User turns unknown accounts (e.g. the Windows login) into guests.
// Without guest shares that only yields a guest session Windows refuses
// with a misleading "network path not found" instead of a password prompt.
mapGuest := "Never"
if anyGuest(c) {
mapGuest = "Bad User"
}
g := []string{
"workgroup = " + c.SMB.Workgroup,
"server string = " + c.SMB.ServerString,
"server role = standalone server",
"security = user",
"map to guest = " + mapGuest,
"guest account = nobody",
"server min protocol = SMB2",
"unix charset = UTF-8",
"logging = file",
"log file = /var/log/samba/log.%m",
"max log size = 1000",
"log level = 1",
"load printers = no",
"printing = bsd",
"printcap name = /dev/null",
"disable spoolss = yes",
"ea support = yes",
"vfs objects = fruit streams_xattr", // macOS clients: resource forks / Finder metadata as xattrs
"fruit:metadata = stream",
"fruit:model = MacSamba",
"dos filemode = yes",
}
for _, l := range g {
fmt.Fprintf(&b, " %s\n", l)
}
for _, s := range c.SMB.Shares {
fmt.Fprintf(&b, "\n[%s]\n", s.Name)
lines := []string{
"path = " + s.Path,
}
if s.Comment != "" {
lines = append(lines, "comment = "+s.Comment)
}
lines = append(lines,
"browseable = "+yesno(s.Browseable),
"read only = "+yesno(s.ReadOnly),
"guest ok = "+yesno(s.Guest),
)
if !s.Guest {
lines = append(lines, "valid users = "+strings.Join(s.Users, " "))
}
lines = append(lines,
"force user = "+owner,
"force group = "+owner,
"create mask = 0664",
"directory mask = 0775",
)
if s.HideSysFiles {
lines = append(lines, "veto files = /._*/.DS_Store/Thumbs.db/desktop.ini/", "delete veto files = yes")
}
for _, l := range lines {
fmt.Fprintf(&b, " %s\n", l)
}
}
return b.String()
}
// RenderExports produces the exports(5) file. uid/gid are the ids squashed
// clients are mapped to.
func RenderExports(c Config, uid, gid string) string {
var b strings.Builder
fmt.Fprintf(&b, "%s\n", Header)
for _, e := range c.NFS.Exports {
opts := []string{"rw"}
if e.ReadOnly {
opts = []string{"ro"}
}
if e.Async {
opts = append(opts, "async")
} else {
opts = append(opts, "sync")
}
switch e.Squash {
case "all":
opts = append(opts, "all_squash", "anonuid="+uid, "anongid="+gid)
case "none":
opts = append(opts, "no_root_squash")
default:
opts = append(opts, "root_squash", "anonuid="+uid, "anongid="+gid)
}
// crossmnt: child mounts (ZFS datasets or other filesystems) must be reachable
opts = append(opts, "crossmnt", "no_subtree_check")
var parts []string
for _, cl := range e.Clients {
parts = append(parts, cl+"("+strings.Join(opts, ",")+")")
}
fmt.Fprintf(&b, "%q %s\n", e.Path, strings.Join(parts, " "))
}
return b.String()
}