smarter client flow
This commit is contained in:
574
app.go
574
app.go
@@ -45,6 +45,7 @@ func (OSCommandRunner) Run(name string, args ...string) error {
|
|||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
configPath string
|
configPath string
|
||||||
|
fstabPath string
|
||||||
users UserManager
|
users UserManager
|
||||||
runner CommandRunner
|
runner CommandRunner
|
||||||
lookPath LookPathFunc
|
lookPath LookPathFunc
|
||||||
@@ -75,9 +76,22 @@ type CIFSMountConfig struct {
|
|||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FstabMountEntry struct {
|
||||||
|
LineIndex int
|
||||||
|
Source string
|
||||||
|
MountPoint string
|
||||||
|
FSType string
|
||||||
|
Options []string
|
||||||
|
Dump string
|
||||||
|
Pass string
|
||||||
|
RawLine string
|
||||||
|
DisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
func NewApp(configPath string, users UserManager, runner CommandRunner, lookPath LookPathFunc) *App {
|
func NewApp(configPath string, users UserManager, runner CommandRunner, lookPath LookPathFunc) *App {
|
||||||
return &App{
|
return &App{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
|
fstabPath: "/etc/fstab",
|
||||||
users: users,
|
users: users,
|
||||||
runner: runner,
|
runner: runner,
|
||||||
lookPath: lookPath,
|
lookPath: lookPath,
|
||||||
@@ -206,63 +220,55 @@ loaded:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) setupClientMount() error {
|
func (a *App) setupClientMount() error {
|
||||||
|
for {
|
||||||
|
entries, err := a.loadCIFSMountEntries()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
a.println("")
|
a.println("")
|
||||||
a.println("Client mount setup")
|
a.println("Client mount setup")
|
||||||
a.println("==================")
|
a.println("==================")
|
||||||
a.println("This will help connect this computer to a shared folder on another server.")
|
a.println("This computer can connect to Samba or CIFS shares listed in /etc/fstab.")
|
||||||
|
a.println("Current client mounts:")
|
||||||
|
if len(entries) == 0 {
|
||||||
|
a.println(" (none)")
|
||||||
|
} else {
|
||||||
|
for i, entry := range entries {
|
||||||
|
a.printf(" %d. %s -> %s\n", i+1, entry.DisplayName, entry.MountPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.println("")
|
||||||
|
a.println("[a] add client mount")
|
||||||
|
a.println("[e] edit client mount")
|
||||||
|
a.println("[d] delete client mount")
|
||||||
|
a.println("[b] back")
|
||||||
a.flush()
|
a.flush()
|
||||||
|
|
||||||
if err := a.ensureCIFSUtilsInstalled(); err != nil {
|
choice, err := a.prompt("Select an action")
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := a.collectCIFSMountConfig()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.ensureMountPoint(cfg.MountPoint); err != nil {
|
switch strings.ToLower(choice) {
|
||||||
|
case "a", "add":
|
||||||
|
if err := a.addClientMount(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "e", "edit":
|
||||||
line := buildFstabLine(cfg)
|
if err := a.editClientMount(entries); err != nil {
|
||||||
a.println("")
|
|
||||||
a.println("Suggested /etc/fstab line:")
|
|
||||||
a.println(line)
|
|
||||||
a.println("")
|
|
||||||
a.println("This will store the username and password directly in /etc/fstab.")
|
|
||||||
a.println("That is simple for home use, but less secure than a credentials file.")
|
|
||||||
a.flush()
|
|
||||||
|
|
||||||
addLine, err := a.confirm("Add this line to /etc/fstab now", true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !addLine {
|
case "d", "delete":
|
||||||
|
if err := a.deleteClientMount(entries); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "b", "back":
|
||||||
return nil
|
return nil
|
||||||
|
default:
|
||||||
|
a.println("Unknown choice.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.appendFstabLine(line); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.println("")
|
|
||||||
a.println("The mount entry was added to /etc/fstab.")
|
|
||||||
mountNow, err := a.confirm("Try mounting it now", true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mountNow {
|
|
||||||
if err := a.reloadSystemdIfNeeded(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := a.mountCIFS(cfg.MountPoint); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.println("The network share was mounted.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) manageUsers(doc *Document) error {
|
func (a *App) manageUsers(doc *Document) error {
|
||||||
@@ -638,53 +644,68 @@ func (a *App) ensureCIFSUtilsInstalled() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) collectCIFSMountConfig() (CIFSMountConfig, error) {
|
func (a *App) collectCIFSMountConfig(defaults CIFSMountConfig) (CIFSMountConfig, error) {
|
||||||
server, err := a.prompt("Server name or IP")
|
server, err := a.promptDefault("Server name or IP", defaults.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
share, err := a.prompt("Share name on that server")
|
share, err := a.promptDefault("Share name on that server", defaults.Share)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
mountPointDefault := filepath.Join("/mnt", strings.TrimSpace(share))
|
mountPointDefault := filepath.Join("/mnt", strings.TrimSpace(share))
|
||||||
|
if defaults.MountPoint != "" {
|
||||||
|
mountPointDefault = defaults.MountPoint
|
||||||
|
}
|
||||||
mountPoint, err := a.promptDefault("Local mount folder", mountPointDefault)
|
mountPoint, err := a.promptDefault("Local mount folder", mountPointDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
username, err := a.prompt("Username for the remote share")
|
username, err := a.promptDefault("Username for the remote share", defaults.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
password, err := a.prompt("Password for the remote share")
|
password, err := a.promptDefault("Password for the remote share", defaults.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
domain, err := a.promptDefault("Domain or workgroup (optional)", "")
|
domain, err := a.promptDefault("Domain or workgroup (optional)", defaults.Domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
uid, err := a.promptDefault("Local owner username or uid (optional)", "")
|
uid, err := a.promptDefault("Local owner username or uid (optional)", defaults.UID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
gid, err := a.promptDefault("Local group name or gid (optional)", "")
|
gid, err := a.promptDefault("Local group name or gid (optional)", defaults.GID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
fileMode, err := a.promptDefault("File permissions", "0664")
|
fileModeDefault := defaults.FileMode
|
||||||
|
if fileModeDefault == "" {
|
||||||
|
fileModeDefault = "0664"
|
||||||
|
}
|
||||||
|
fileMode, err := a.promptDefault("File permissions", fileModeDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
dirMode, err := a.promptDefault("Folder permissions", "0775")
|
dirModeDefault := defaults.DirMode
|
||||||
|
if dirModeDefault == "" {
|
||||||
|
dirModeDefault = "0775"
|
||||||
|
}
|
||||||
|
dirMode, err := a.promptDefault("Folder permissions", dirModeDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
autoMount, err := a.confirm("Mount automatically at startup", true)
|
autoMountDefault := true
|
||||||
|
if defaults.Server != "" || defaults.Share != "" || defaults.MountPoint != "" {
|
||||||
|
autoMountDefault = defaults.AutoMount
|
||||||
|
}
|
||||||
|
autoMount, err := a.confirm("Mount automatically at startup", autoMountDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
readOnly, err := a.confirm("Make this mount read-only", false)
|
readOnly, err := a.confirm("Make this mount read-only", defaults.ReadOnly)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CIFSMountConfig{}, err
|
return CIFSMountConfig{}, err
|
||||||
}
|
}
|
||||||
@@ -736,45 +757,6 @@ func (a *App) ensureMountPoint(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) appendFstabLine(line string) error {
|
|
||||||
content := line + "\n"
|
|
||||||
file, err := os.OpenFile("/etc/fstab", os.O_APPEND|os.O_WRONLY, 0)
|
|
||||||
if err == nil {
|
|
||||||
defer file.Close()
|
|
||||||
if _, err := file.WriteString(content); err == nil {
|
|
||||||
return nil
|
|
||||||
} else if !shouldOfferPrivilegeRetry(err) {
|
|
||||||
return fmt.Errorf("append mount entry to /etc/fstab: %w", err)
|
|
||||||
}
|
|
||||||
} else if !shouldOfferPrivilegeRetry(err) {
|
|
||||||
return fmt.Errorf("open /etc/fstab for update: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.println("")
|
|
||||||
a.println("Updating /etc/fstab needs administrator permission.")
|
|
||||||
a.println("I can try again using sudo so you can enter your admin password.")
|
|
||||||
a.flush()
|
|
||||||
|
|
||||||
retry, err := a.confirm("Retry updating /etc/fstab with sudo", true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !retry {
|
|
||||||
return errors.New("updating /etc/fstab was cancelled")
|
|
||||||
}
|
|
||||||
|
|
||||||
tempPath, err := a.writeTempConfig(content)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("prepare fstab update: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tempPath)
|
|
||||||
|
|
||||||
if err := a.runner.Run("sudo", "sh", "-c", fmt.Sprintf("cat %s >> /etc/fstab", shellQuote(tempPath))); err != nil {
|
|
||||||
return fmt.Errorf("append mount entry to /etc/fstab with sudo: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) mountCIFS(path string) error {
|
func (a *App) mountCIFS(path string) error {
|
||||||
if err := a.runPrivilegedOrLocal("mount", []string{path}, "Mounting the network share needs administrator permission."); err != nil {
|
if err := a.runPrivilegedOrLocal("mount", []string{path}, "Mounting the network share needs administrator permission."); err != nil {
|
||||||
return fmt.Errorf("mount %s: %w", path, err)
|
return fmt.Errorf("mount %s: %w", path, err)
|
||||||
@@ -1432,6 +1414,414 @@ func shellQuote(value string) string {
|
|||||||
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
|
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) loadCIFSMountEntries() ([]FstabMountEntry, error) {
|
||||||
|
data, err := os.ReadFile(a.fstabPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read %s: %w", a.fstabPath, err)
|
||||||
|
}
|
||||||
|
return parseCIFSMountEntries(string(data)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCIFSMountEntries(contents string) []FstabMountEntry {
|
||||||
|
lines := strings.Split(contents, "\n")
|
||||||
|
entries := make([]FstabMountEntry, 0)
|
||||||
|
for index, line := range lines {
|
||||||
|
entry, ok := parseFstabMountLine(line, index)
|
||||||
|
if ok {
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFstabMountLine(line string, index int) (FstabMountEntry, bool) {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||||
|
return FstabMountEntry{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Fields(trimmed)
|
||||||
|
if len(fields) < 4 {
|
||||||
|
return FstabMountEntry{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
fsType := strings.ToLower(fields[2])
|
||||||
|
if fsType != "cifs" && fsType != "smbfs" {
|
||||||
|
return FstabMountEntry{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := FstabMountEntry{
|
||||||
|
LineIndex: index,
|
||||||
|
Source: fields[0],
|
||||||
|
MountPoint: unescapeFstabValue(fields[1]),
|
||||||
|
FSType: fields[2],
|
||||||
|
Options: splitFstabOptions(fields[3]),
|
||||||
|
RawLine: line,
|
||||||
|
DisplayName: fmt.Sprintf(
|
||||||
|
"%s (%s)",
|
||||||
|
unescapeFstabValue(fields[0]),
|
||||||
|
unescapeFstabValue(fields[2]),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if len(fields) > 4 {
|
||||||
|
entry.Dump = fields[4]
|
||||||
|
}
|
||||||
|
if len(fields) > 5 {
|
||||||
|
entry.Pass = fields[5]
|
||||||
|
}
|
||||||
|
if entry.Dump == "" {
|
||||||
|
entry.Dump = "0"
|
||||||
|
}
|
||||||
|
if entry.Pass == "" {
|
||||||
|
entry.Pass = "0"
|
||||||
|
}
|
||||||
|
if entry.DisplayName == " (cifs)" || entry.DisplayName == " (smbfs)" {
|
||||||
|
entry.DisplayName = entry.MountPoint
|
||||||
|
}
|
||||||
|
return entry, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitFstabOptions(value string) []string {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var options []string
|
||||||
|
var current strings.Builder
|
||||||
|
escaped := false
|
||||||
|
for _, r := range value {
|
||||||
|
switch {
|
||||||
|
case escaped:
|
||||||
|
current.WriteRune(r)
|
||||||
|
escaped = false
|
||||||
|
case r == '\\':
|
||||||
|
current.WriteRune(r)
|
||||||
|
escaped = true
|
||||||
|
case r == ',':
|
||||||
|
options = append(options, current.String())
|
||||||
|
current.Reset()
|
||||||
|
default:
|
||||||
|
current.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options = append(options, current.String())
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func unescapeFstabValue(value string) string {
|
||||||
|
value = strings.ReplaceAll(value, `\040`, " ")
|
||||||
|
value = strings.ReplaceAll(value, `\,`, ",")
|
||||||
|
value = strings.ReplaceAll(value, `\\`, `\`)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) addClientMount() error {
|
||||||
|
if err := a.ensureCIFSUtilsInstalled(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := a.collectCIFSMountConfig(CIFSMountConfig{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.ensureMountPoint(cfg.MountPoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
line := buildFstabLine(cfg)
|
||||||
|
if err := a.confirmAndWriteClientMount(line, -1, "Add this line to /etc/fstab now"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.println("")
|
||||||
|
a.println("The mount entry was added to /etc/fstab.")
|
||||||
|
return a.offerMountNow(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) editClientMount(entries []FstabMountEntry) error {
|
||||||
|
entry, err := a.selectCIFSMount(entries)
|
||||||
|
if err != nil || entry == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := cifsMountConfigFromEntry(*entry)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not read mount details from %s", entry.RawLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.ensureCIFSUtilsInstalled(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := a.collectCIFSMountConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.ensureMountPoint(updated.MountPoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
line := buildFstabLine(updated)
|
||||||
|
if err := a.confirmAndWriteClientMount(line, entry.LineIndex, "Save these changes to /etc/fstab now"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.println("")
|
||||||
|
a.println("The mount entry was updated in /etc/fstab.")
|
||||||
|
return a.offerMountNow(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) deleteClientMount(entries []FstabMountEntry) error {
|
||||||
|
entry, err := a.selectCIFSMount(entries)
|
||||||
|
if err != nil || entry == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm, err := a.confirm(fmt.Sprintf("Delete the mount for %s", entry.DisplayName), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !confirm {
|
||||||
|
a.println("Delete cancelled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.writeFstabLineChange("", entry.LineIndex); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.println("The mount entry was removed from /etc/fstab.")
|
||||||
|
return a.reloadSystemdIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) selectCIFSMount(entries []FstabMountEntry) (*FstabMountEntry, error) {
|
||||||
|
if len(entries) == 0 {
|
||||||
|
a.println("There are no client mounts to select.")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
selection, err := a.prompt("Select mount number")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
index, err := strconv.Atoi(selection)
|
||||||
|
if err != nil || index < 1 || index > len(entries) {
|
||||||
|
a.println("Invalid selection.")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := entries[index-1]
|
||||||
|
return &entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cifsMountConfigFromEntry(entry FstabMountEntry) (CIFSMountConfig, bool) {
|
||||||
|
server, share, ok := splitCIFSSource(entry.Source)
|
||||||
|
if !ok {
|
||||||
|
return CIFSMountConfig{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := CIFSMountConfig{
|
||||||
|
Server: server,
|
||||||
|
Share: share,
|
||||||
|
MountPoint: entry.MountPoint,
|
||||||
|
AutoMount: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range entry.Options {
|
||||||
|
key, value, hasValue := strings.Cut(option, "=")
|
||||||
|
switch strings.ToLower(key) {
|
||||||
|
case "username":
|
||||||
|
if hasValue {
|
||||||
|
cfg.Username = unescapeFstabValue(value)
|
||||||
|
}
|
||||||
|
case "password":
|
||||||
|
if hasValue {
|
||||||
|
cfg.Password = unescapeFstabValue(value)
|
||||||
|
}
|
||||||
|
case "domain":
|
||||||
|
if hasValue {
|
||||||
|
cfg.Domain = unescapeFstabValue(value)
|
||||||
|
}
|
||||||
|
case "uid":
|
||||||
|
if hasValue {
|
||||||
|
cfg.UID = unescapeFstabValue(value)
|
||||||
|
}
|
||||||
|
case "gid":
|
||||||
|
if hasValue {
|
||||||
|
cfg.GID = unescapeFstabValue(value)
|
||||||
|
}
|
||||||
|
case "file_mode":
|
||||||
|
if hasValue {
|
||||||
|
cfg.FileMode = value
|
||||||
|
}
|
||||||
|
case "dir_mode":
|
||||||
|
if hasValue {
|
||||||
|
cfg.DirMode = value
|
||||||
|
}
|
||||||
|
case "ro":
|
||||||
|
cfg.ReadOnly = true
|
||||||
|
case "rw":
|
||||||
|
cfg.ReadOnly = false
|
||||||
|
case "noauto":
|
||||||
|
cfg.AutoMount = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitCIFSSource(source string) (server, share string, ok bool) {
|
||||||
|
unescaped := unescapeFstabValue(source)
|
||||||
|
if !strings.HasPrefix(unescaped, "//") {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(strings.TrimPrefix(unescaped, "//"), "/", 2)
|
||||||
|
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[0], parts[1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) confirmAndWriteClientMount(line string, lineIndex int, promptLabel string) error {
|
||||||
|
a.println("")
|
||||||
|
a.println("Suggested /etc/fstab line:")
|
||||||
|
a.println(line)
|
||||||
|
a.println("")
|
||||||
|
a.println("This will store the username and password directly in /etc/fstab.")
|
||||||
|
a.println("That is simple for home use, but less secure than a credentials file.")
|
||||||
|
a.flush()
|
||||||
|
|
||||||
|
confirm, err := a.confirm(promptLabel, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !confirm {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.writeFstabLineChange(line, lineIndex); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return a.reloadSystemdIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) offerMountNow(cfg CIFSMountConfig) error {
|
||||||
|
mountNow, err := a.confirm("Try mounting it now", true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !mountNow {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := a.mountCIFS(cfg.MountPoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.println("The network share was mounted.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) writeFstabLineChange(line string, lineIndex int) error {
|
||||||
|
data, err := os.ReadFile(a.fstabPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read %s: %w", a.fstabPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := updateFstabContents(string(data), line, lineIndex)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return a.writeFstabContents(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFstabContents(contents, line string, lineIndex int) (string, error) {
|
||||||
|
hasTrailingNewline := strings.HasSuffix(contents, "\n")
|
||||||
|
lines := strings.Split(contents, "\n")
|
||||||
|
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
||||||
|
lines = lines[:len(lines)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case lineIndex < 0:
|
||||||
|
lines = append(lines, line)
|
||||||
|
case lineIndex >= len(lines):
|
||||||
|
return "", fmt.Errorf("mount entry line %d is out of range", lineIndex)
|
||||||
|
case line == "":
|
||||||
|
lines = append(lines[:lineIndex], lines[lineIndex+1:]...)
|
||||||
|
default:
|
||||||
|
lines[lineIndex] = line
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := strings.Join(lines, "\n")
|
||||||
|
if hasTrailingNewline || updated != "" {
|
||||||
|
updated += "\n"
|
||||||
|
}
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) writeFstabContents(serialized string) error {
|
||||||
|
backup := fmt.Sprintf("%s.bak.%s", a.fstabPath, time.Now().UTC().Format("20060102T150405Z"))
|
||||||
|
|
||||||
|
if err := copyFile(a.fstabPath, backup); err != nil {
|
||||||
|
if shouldOfferPrivilegeRetry(err) {
|
||||||
|
return a.writeFstabWithPrivilegeRetry(serialized, backup)
|
||||||
|
}
|
||||||
|
return friendlyWriteError("create backup", backup, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(a.fstabPath, []byte(serialized), 0o644); err != nil {
|
||||||
|
if shouldOfferPrivilegeRetry(err) {
|
||||||
|
return a.writeFstabWithPrivilegeRetry(serialized, backup)
|
||||||
|
}
|
||||||
|
return friendlyWriteError("write config", a.fstabPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) writeFstabWithPrivilegeRetry(serialized, backup string) error {
|
||||||
|
a.println("")
|
||||||
|
a.println("Updating /etc/fstab usually 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 friendlyWriteError("write config", a.fstabPath, os.ErrPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
retry, err := a.confirm("Retry updating /etc/fstab with sudo", true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !retry {
|
||||||
|
return friendlyWriteError("write config", a.fstabPath, os.ErrPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempPath, err := a.writeTempConfig(serialized)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("prepare fstab for sudo save: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempPath)
|
||||||
|
|
||||||
|
if err := a.runner.Run("sudo", "cp", a.fstabPath, backup); err != nil {
|
||||||
|
return friendlyWriteError("create backup", backup, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.runner.Run("sudo", "install", "-m", "644", tempPath, a.fstabPath); err != nil {
|
||||||
|
return friendlyWriteError("write config", a.fstabPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ShareUserReference struct {
|
type ShareUserReference struct {
|
||||||
User string
|
User string
|
||||||
Shares []string
|
Shares []string
|
||||||
|
|||||||
88
app_test.go
Normal file
88
app_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseCIFSMountEntries(t *testing.T) {
|
||||||
|
contents := `# comment
|
||||||
|
UUID=1234 / ext4 defaults 0 1
|
||||||
|
//fileserver/media /mnt/media cifs username=alice,password=p@\040ss\,word,iocharset=utf8,uid=1000,gid=1000,file_mode=0664,dir_mode=0775,rw 0 0
|
||||||
|
//nas/backups /srv/backups smbfs username=bob,password=secret,noauto,ro 0 0
|
||||||
|
`
|
||||||
|
|
||||||
|
entries := parseCIFSMountEntries(contents)
|
||||||
|
if len(entries) != 2 {
|
||||||
|
t.Fatalf("expected 2 CIFS entries, got %d", len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if entries[0].LineIndex != 2 {
|
||||||
|
t.Fatalf("unexpected first line index: %d", entries[0].LineIndex)
|
||||||
|
}
|
||||||
|
if entries[0].Source != "//fileserver/media" {
|
||||||
|
t.Fatalf("unexpected first source: %q", entries[0].Source)
|
||||||
|
}
|
||||||
|
if entries[0].MountPoint != "/mnt/media" {
|
||||||
|
t.Fatalf("unexpected first mount point: %q", entries[0].MountPoint)
|
||||||
|
}
|
||||||
|
if entries[1].FSType != "smbfs" {
|
||||||
|
t.Fatalf("unexpected second fs type: %q", entries[1].FSType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCIFSMountConfigFromEntry(t *testing.T) {
|
||||||
|
entry, ok := parseFstabMountLine("//fileserver/media /mnt/media cifs username=alice,password=p@\\040ss\\,word,domain=WORKGROUP,uid=1000,gid=1000,file_mode=0664,dir_mode=0775,noauto,ro 0 0", 0)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected line to parse")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := cifsMountConfigFromEntry(entry)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected config conversion to succeed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Server != "fileserver" || cfg.Share != "media" {
|
||||||
|
t.Fatalf("unexpected source parsing: %+v", cfg)
|
||||||
|
}
|
||||||
|
if cfg.Password != "p@ ss,word" {
|
||||||
|
t.Fatalf("unexpected password: %q", cfg.Password)
|
||||||
|
}
|
||||||
|
if cfg.Domain != "WORKGROUP" {
|
||||||
|
t.Fatalf("unexpected domain: %q", cfg.Domain)
|
||||||
|
}
|
||||||
|
if cfg.AutoMount {
|
||||||
|
t.Fatal("expected noauto to disable automount")
|
||||||
|
}
|
||||||
|
if !cfg.ReadOnly {
|
||||||
|
t.Fatal("expected ro to set read-only")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateFstabContentsAddEditDelete(t *testing.T) {
|
||||||
|
initial := "# header\nUUID=1234 / ext4 defaults 0 1\n//old/share /mnt/share cifs username=old,password=old,rw 0 0\n"
|
||||||
|
|
||||||
|
added, err := updateFstabContents(initial, "//new/share /mnt/new cifs username=new,password=new,rw 0 0", -1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("add entry: %v", err)
|
||||||
|
}
|
||||||
|
wantAdded := "# header\nUUID=1234 / ext4 defaults 0 1\n//old/share /mnt/share cifs username=old,password=old,rw 0 0\n//new/share /mnt/new cifs username=new,password=new,rw 0 0\n"
|
||||||
|
if added != wantAdded {
|
||||||
|
t.Fatalf("unexpected add result:\nwant: %q\ngot: %q", wantAdded, added)
|
||||||
|
}
|
||||||
|
|
||||||
|
edited, err := updateFstabContents(initial, "//old/share /mnt/share cifs username=alice,password=secret,ro 0 0", 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("edit entry: %v", err)
|
||||||
|
}
|
||||||
|
wantEdited := "# header\nUUID=1234 / ext4 defaults 0 1\n//old/share /mnt/share cifs username=alice,password=secret,ro 0 0\n"
|
||||||
|
if edited != wantEdited {
|
||||||
|
t.Fatalf("unexpected edit result:\nwant: %q\ngot: %q", wantEdited, edited)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted, err := updateFstabContents(initial, "", 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("delete entry: %v", err)
|
||||||
|
}
|
||||||
|
wantDeleted := "# header\nUUID=1234 / ext4 defaults 0 1\n"
|
||||||
|
if deleted != wantDeleted {
|
||||||
|
t.Fatalf("unexpected delete result:\nwant: %q\ngot: %q", wantDeleted, deleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
samba-configer
BIN
samba-configer
Binary file not shown.
Reference in New Issue
Block a user