smarter client flow
This commit is contained in:
596
app.go
596
app.go
@@ -45,6 +45,7 @@ func (OSCommandRunner) Run(name string, args ...string) error {
|
||||
|
||||
type App struct {
|
||||
configPath string
|
||||
fstabPath string
|
||||
users UserManager
|
||||
runner CommandRunner
|
||||
lookPath LookPathFunc
|
||||
@@ -75,9 +76,22 @@ type CIFSMountConfig struct {
|
||||
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 {
|
||||
return &App{
|
||||
configPath: configPath,
|
||||
fstabPath: "/etc/fstab",
|
||||
users: users,
|
||||
runner: runner,
|
||||
lookPath: lookPath,
|
||||
@@ -206,63 +220,55 @@ loaded:
|
||||
}
|
||||
|
||||
func (a *App) setupClientMount() error {
|
||||
a.println("")
|
||||
a.println("Client mount setup")
|
||||
a.println("==================")
|
||||
a.println("This will help connect this computer to a shared folder on another server.")
|
||||
a.flush()
|
||||
|
||||
if err := a.ensureCIFSUtilsInstalled(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := a.collectCIFSMountConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.ensureMountPoint(cfg.MountPoint); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
line := buildFstabLine(cfg)
|
||||
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
|
||||
}
|
||||
if !addLine {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
for {
|
||||
entries, err := a.loadCIFSMountEntries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.mountCIFS(cfg.MountPoint); err != nil {
|
||||
|
||||
a.println("")
|
||||
a.println("Client mount setup")
|
||||
a.println("==================")
|
||||
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()
|
||||
|
||||
choice, err := a.prompt("Select an action")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.println("The network share was mounted.")
|
||||
}
|
||||
|
||||
return nil
|
||||
switch strings.ToLower(choice) {
|
||||
case "a", "add":
|
||||
if err := a.addClientMount(); err != nil {
|
||||
return err
|
||||
}
|
||||
case "e", "edit":
|
||||
if err := a.editClientMount(entries); err != nil {
|
||||
return err
|
||||
}
|
||||
case "d", "delete":
|
||||
if err := a.deleteClientMount(entries); err != nil {
|
||||
return err
|
||||
}
|
||||
case "b", "back":
|
||||
return nil
|
||||
default:
|
||||
a.println("Unknown choice.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) manageUsers(doc *Document) error {
|
||||
@@ -638,53 +644,68 @@ func (a *App) ensureCIFSUtilsInstalled() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) collectCIFSMountConfig() (CIFSMountConfig, error) {
|
||||
server, err := a.prompt("Server name or IP")
|
||||
func (a *App) collectCIFSMountConfig(defaults CIFSMountConfig) (CIFSMountConfig, error) {
|
||||
server, err := a.promptDefault("Server name or IP", defaults.Server)
|
||||
if err != nil {
|
||||
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 {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
mountPointDefault := filepath.Join("/mnt", strings.TrimSpace(share))
|
||||
if defaults.MountPoint != "" {
|
||||
mountPointDefault = defaults.MountPoint
|
||||
}
|
||||
mountPoint, err := a.promptDefault("Local mount folder", mountPointDefault)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
domain, err := a.promptDefault("Domain or workgroup (optional)", "")
|
||||
domain, err := a.promptDefault("Domain or workgroup (optional)", defaults.Domain)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
@@ -736,45 +757,6 @@ func (a *App) ensureMountPoint(path string) error {
|
||||
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 {
|
||||
if err := a.runPrivilegedOrLocal("mount", []string{path}, "Mounting the network share needs administrator permission."); err != nil {
|
||||
return fmt.Errorf("mount %s: %w", path, err)
|
||||
@@ -1432,6 +1414,414 @@ func shellQuote(value string) string {
|
||||
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 {
|
||||
User string
|
||||
Shares []string
|
||||
|
||||
Reference in New Issue
Block a user