client flow
This commit is contained in:
310
app.go
310
app.go
@@ -60,6 +60,21 @@ type PasswdEntry struct {
|
||||
Shell string
|
||||
}
|
||||
|
||||
type CIFSMountConfig struct {
|
||||
Server string
|
||||
Share string
|
||||
MountPoint string
|
||||
Username string
|
||||
Password string
|
||||
Domain string
|
||||
UID string
|
||||
GID string
|
||||
FileMode string
|
||||
DirMode string
|
||||
AutoMount bool
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
func NewApp(configPath string, users UserManager, runner CommandRunner, lookPath LookPathFunc) *App {
|
||||
return &App{
|
||||
configPath: configPath,
|
||||
@@ -103,6 +118,7 @@ loaded:
|
||||
a.println("[a] add share")
|
||||
a.println("[e] edit share")
|
||||
a.println("[d] delete share")
|
||||
a.println("[m] set up client mount")
|
||||
a.println("[u] manage users")
|
||||
a.println("[w] write config and exit")
|
||||
a.println("[q] quit without saving")
|
||||
@@ -126,6 +142,10 @@ loaded:
|
||||
if err := a.deleteShare(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
case "m", "mount":
|
||||
if err := a.setupClientMount(); err != nil {
|
||||
return err
|
||||
}
|
||||
case "u", "users":
|
||||
if err := a.manageUsers(doc); err != nil {
|
||||
return err
|
||||
@@ -140,6 +160,63 @@ 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.mountCIFS(cfg.MountPoint); err != nil {
|
||||
return err
|
||||
}
|
||||
a.println("The network share was mounted.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) manageUsers(doc *Document) error {
|
||||
for {
|
||||
a.println("")
|
||||
@@ -471,6 +548,192 @@ func (a *App) writeConfig(doc *Document) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ensureCIFSUtilsInstalled() error {
|
||||
if a.lookPath != nil {
|
||||
if _, err := a.lookPath("mount.cifs"); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
a.println("")
|
||||
a.println("This computer does not seem to have the CIFS mount tools installed yet.")
|
||||
|
||||
plan, ok := DetectCIFSUtilsInstallPlan(a.lookPath, os.Geteuid() == 0)
|
||||
if !ok {
|
||||
a.println("I couldn't find a supported package manager automatically.")
|
||||
a.println("Install cifs-utils for your Linux distribution, then try again.")
|
||||
return errors.New("cifs-utils is not installed")
|
||||
}
|
||||
|
||||
a.printf("I found %s and can try to install the CIFS client tools for you.\n", plan.ManagerName)
|
||||
a.printf("This would run: %s\n", plan.DisplayCommand())
|
||||
a.flush()
|
||||
|
||||
install, err := a.confirm("Install the CIFS client tools now", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !install {
|
||||
return errors.New("cifs-utils is required for client mounts")
|
||||
}
|
||||
|
||||
if err := a.runner.Run(plan.Command[0], plan.Command[1:]...); err != nil {
|
||||
return fmt.Errorf("install CIFS client tools with %s: %w", plan.ManagerName, err)
|
||||
}
|
||||
|
||||
if a.lookPath != nil {
|
||||
if _, err := a.lookPath("mount.cifs"); err != nil {
|
||||
return fmt.Errorf("cifs-utils installation finished, but mount.cifs is still not available: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) collectCIFSMountConfig() (CIFSMountConfig, error) {
|
||||
server, err := a.prompt("Server name or IP")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
share, err := a.prompt("Share name on that server")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
mountPointDefault := filepath.Join("/mnt", strings.TrimSpace(share))
|
||||
mountPoint, err := a.promptDefault("Local mount folder", mountPointDefault)
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
username, err := a.prompt("Username for the remote share")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
password, err := a.prompt("Password for the remote share")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
domain, err := a.promptDefault("Domain or workgroup (optional)", "")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
uid, err := a.promptDefault("Local owner username or uid (optional)", "")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
gid, err := a.promptDefault("Local group name or gid (optional)", "")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
fileMode, err := a.promptDefault("File permissions", "0664")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
dirMode, err := a.promptDefault("Folder permissions", "0775")
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
autoMount, err := a.confirm("Mount automatically at startup", true)
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
readOnly, err := a.confirm("Make this mount read-only", false)
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
|
||||
return CIFSMountConfig{
|
||||
Server: strings.TrimSpace(server),
|
||||
Share: strings.TrimSpace(share),
|
||||
MountPoint: filepath.Clean(strings.TrimSpace(mountPoint)),
|
||||
Username: strings.TrimSpace(username),
|
||||
Password: strings.TrimSpace(password),
|
||||
Domain: strings.TrimSpace(domain),
|
||||
UID: strings.TrimSpace(uid),
|
||||
GID: strings.TrimSpace(gid),
|
||||
FileMode: strings.TrimSpace(fileMode),
|
||||
DirMode: strings.TrimSpace(dirMode),
|
||||
AutoMount: autoMount,
|
||||
ReadOnly: readOnly,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *App) ensureMountPoint(path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("mount folder exists but is not a directory: %s", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("check mount folder %s: %w", path, err)
|
||||
}
|
||||
|
||||
a.println("")
|
||||
a.printf("The local mount folder %s does not exist yet.\n", path)
|
||||
a.println("I can create it now.")
|
||||
a.flush()
|
||||
|
||||
create, err := a.confirm("Create this folder now", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !create {
|
||||
return errors.New("a mount folder is needed before continuing")
|
||||
}
|
||||
|
||||
if err := a.createDirectory(path); err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ensureShareDirectories(doc *Document) error {
|
||||
for _, section := range doc.ShareSections() {
|
||||
cfg := ShareFromSection(section)
|
||||
@@ -1054,6 +1317,53 @@ func friendlyWriteError(action, path string, err error) error {
|
||||
return fmt.Errorf("%s %s: %w", action, path, err)
|
||||
}
|
||||
|
||||
func buildFstabLine(cfg CIFSMountConfig) string {
|
||||
source := fmt.Sprintf("//%s/%s", cfg.Server, cfg.Share)
|
||||
|
||||
options := []string{
|
||||
"username=" + escapeFstabValue(cfg.Username),
|
||||
"password=" + escapeFstabValue(cfg.Password),
|
||||
"iocharset=utf8",
|
||||
}
|
||||
|
||||
if cfg.Domain != "" {
|
||||
options = append(options, "domain="+escapeFstabValue(cfg.Domain))
|
||||
}
|
||||
if cfg.UID != "" {
|
||||
options = append(options, "uid="+escapeFstabValue(cfg.UID))
|
||||
}
|
||||
if cfg.GID != "" {
|
||||
options = append(options, "gid="+escapeFstabValue(cfg.GID))
|
||||
}
|
||||
if cfg.FileMode != "" {
|
||||
options = append(options, "file_mode="+cfg.FileMode)
|
||||
}
|
||||
if cfg.DirMode != "" {
|
||||
options = append(options, "dir_mode="+cfg.DirMode)
|
||||
}
|
||||
if cfg.ReadOnly {
|
||||
options = append(options, "ro")
|
||||
} else {
|
||||
options = append(options, "rw")
|
||||
}
|
||||
if !cfg.AutoMount {
|
||||
options = append(options, "noauto")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s cifs %s 0 0", source, cfg.MountPoint, strings.Join(options, ","))
|
||||
}
|
||||
|
||||
func escapeFstabValue(value string) string {
|
||||
value = strings.ReplaceAll(value, `\`, `\\`)
|
||||
value = strings.ReplaceAll(value, ",", `\,`)
|
||||
value = strings.ReplaceAll(value, " ", `\040`)
|
||||
return value
|
||||
}
|
||||
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
type ShareUserReference struct {
|
||||
User string
|
||||
Shares []string
|
||||
|
||||
Reference in New Issue
Block a user