diff --git a/cfg/configParser.go b/cfg/configParser.go index 8471171..4890a0e 100644 --- a/cfg/configParser.go +++ b/cfg/configParser.go @@ -1,6 +1,7 @@ package cfg import ( + "errors" "fmt" "github.com/rwinkhart/go-boilerplate/back" @@ -10,12 +11,12 @@ import ( // loadConfig loads the libmutton.ini file and returns the configuration. // It is a utility function for ParseConfig and WriteConfig; do not call directly. -func loadConfig() *ini.File { +func loadConfig() (*ini.File, error) { cfg, err := ini.Load(global.ConfigPath) if err != nil { - back.PrintError("Failed to load libmutton.ini: "+err.Error(), back.ErrorRead, true) + return nil, errors.New("unable to load libmutton.ini: " + err.Error()) } - return cfg + return cfg, nil } // ParseConfig reads the libmutton.ini file and returns a slice of values for the specified keys. @@ -25,7 +26,10 @@ func loadConfig() *ini.File { // error (nil if no error occurred, otherwise an error using the generated or provided message). func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]string, error) { var err error - cfg := loadConfig() + cfg, err := loadConfig() + if err != nil { + return nil, err + } var config []string @@ -36,14 +40,12 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin if value == "" { switch missingValueError { case "": - err = fmt.Errorf("failed to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0]) + err = fmt.Errorf("unable to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0]) case "0": back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently default: err = fmt.Errorf("%s", missingValueError) } - back.PrintError(err.Error(), back.ErrorRead, false) - // if interactive (soft exit), return nil and the error to be handled by the caller return nil, err } @@ -57,12 +59,16 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin // Requires: valuesToWrite (a slice of length 3 arrays each containing a section, a key name, and a value), // prune (a slice similar to valuesToWrite to allow removing the specified keys from an existing config), // append (set to true to append to the existing libmutton.ini file, false to overwrite it). -func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool) { +func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool) error { var cfg *ini.File + var err error if append { // load existing ini file - cfg = loadConfig() + cfg, err = loadConfig() + if err != nil { + return errors.New("unable to load existing libmutton.ini: " + err.Error()) + } } else { // create empty ini container cfg = ini.Empty() @@ -93,8 +99,9 @@ func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool // save to libmutton.ini setUmask(0077) // only give permissions to owner - err := cfg.SaveTo(global.ConfigPath) + err = cfg.SaveTo(global.ConfigPath) if err != nil { - back.PrintError("Failed to save libmutton.ini: "+err.Error(), back.ErrorWrite, true) + return errors.New("unable to save libmutton.ini: " + err.Error()) } + return nil } diff --git a/core/clipClearProcessGeneric.go b/core/clipClearProcessGeneric.go index 2e63653..9dcbf00 100644 --- a/core/clipClearProcessGeneric.go +++ b/core/clipClearProcessGeneric.go @@ -3,30 +3,31 @@ package core import ( + "errors" "strings" "time" "github.com/rwinkhart/go-boilerplate/back" - "github.com/rwinkhart/libmutton/global" ) // clipClearProcess clears the clipboard after 30 seconds if the clipboard contents have not changed. // assignedContents can be omitted to clear the clipboard immediately and unconditionally. -func clipClearProcess(assignedContents string) { +func clipClearProcess(assignedContents string) error { cmdPaste, cmdClear := getClipCommands() - clearClipboard := func() { + clearClipboard := func() error { err := cmdClear.Run() if err != nil { - back.PrintError("Failed to clear clipboard", global.ErrorClipboard, true) + return errors.New("unable to clear clipboard") } back.Exit(0) + return nil } // if assignedContents is empty, clear the clipboard immediately and unconditionally if assignedContents == "" { clearClipboard() - return + return nil } // wait 30 seconds before checking clipboard contents @@ -34,10 +35,14 @@ func clipClearProcess(assignedContents string) { newContents, err := cmdPaste.Output() if err != nil { - back.PrintError("Failed to read clipboard contents", global.ErrorClipboard, true) + return errors.New("unable to read clipboard contents") } if assignedContents == strings.TrimRight(string(newContents), "\r\n") { - clearClipboard() + err := clearClipboard() + if err != nil { + return err + } } + return nil } diff --git a/core/copy.go b/core/copy.go index 36c4f48..47f67e4 100644 --- a/core/copy.go +++ b/core/copy.go @@ -1,6 +1,7 @@ package core import ( + "errors" "fmt" "os" "strings" @@ -13,10 +14,13 @@ import ( ) // CopyArgument copies a field from an entry to the clipboard. -func CopyArgument(targetLocation string, field int) { +func CopyArgument(targetLocation string, field int) error { if isFile, _ := back.TargetIsFile(targetLocation, true, 2); isFile { - decryptedEntry := crypt.DecryptFileToSlice(targetLocation) + decryptedEntry, err := crypt.DecryptFileToSlice(targetLocation) + if err != nil { + return errors.New("unable to decrypt entry: " + err.Error()) + } var copySubject string // will store data to be copied // ensure field exists in entry @@ -24,7 +28,7 @@ func CopyArgument(targetLocation string, field int) { // ensure field is not empty if decryptedEntry[field] == "" { - back.PrintError("Field is empty", back.ErrorTargetNotFound, true) + return errors.New("field is empty") } if field != 2 { @@ -44,18 +48,23 @@ func CopyArgument(targetLocation string, field int) { for { // keep token copied to clipboard, refresh on 30-second intervals currentTime := time.Now() - copyString(true, GenTOTP(secret, currentTime, forSteam)) + token, err := GenTOTP(secret, currentTime, forSteam) + if err != nil { + return err + } + copyString(true, token) // sleep until next 30-second interval time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second) } } } else { - back.PrintError("Field does not exist in entry", back.ErrorTargetNotFound, true) + return errors.New("field does not exist in entry") } // copy field to clipboard, launch clipboard clearing process copyString(false, copySubject) } + return nil } // ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess. @@ -68,7 +77,7 @@ func ClipClearArgument() { } // GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP). -func GenTOTP(secret string, time time.Time, forSteam bool) string { +func GenTOTP(secret string, time time.Time, forSteam bool) (string, error) { var totpToken string var err error @@ -79,8 +88,8 @@ func GenTOTP(secret string, time time.Time, forSteam bool) string { } if err != nil { - back.PrintError("Error generating TOTP code: "+err.Error(), back.ErrorOther, true) + return "", errors.New("error generating TOTP code: " + err.Error()) } - return totpToken + return totpToken, nil } diff --git a/core/copyDARWIN.go b/core/copyDARWIN.go index 00e9579..f8b6236 100644 --- a/core/copyDARWIN.go +++ b/core/copyDARWIN.go @@ -3,24 +3,24 @@ package core import ( + "errors" "os/exec" "github.com/rwinkhart/go-boilerplate/back" - "github.com/rwinkhart/libmutton/global" ) // copyString copies a string to the clipboard. -func copyString(continuous bool, copySubject string) { +func copyString(continuous bool, copySubject string) error { cmd := exec.Command("pbcopy") back.WriteToStdin(cmd, copySubject) err := cmd.Run() if err != nil { - back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true) + return errors.New("unable to copy to clipboard: " + err.Error()) } - if !continuous { LaunchClipClearProcess(copySubject) } + return nil } // getClipCommands returns the commands for pasting and clearing the clipboard contents. diff --git a/core/copyDROID.go b/core/copyDROID.go index e065518..93a839c 100644 --- a/core/copyDROID.go +++ b/core/copyDROID.go @@ -6,8 +6,6 @@ import ( "golang.design/x/clipboard" ) -// TODO Investigate background clipboard clearing and on-app-close clipboard clearing for Android - // copyString copies a string to the clipboard. func copyString(continuous bool, copySubject string) { clipboard.Write(clipboard.FmtText, []byte(copySubject)) diff --git a/core/copyTERMUX.go b/core/copyTERMUX.go index 1cab319..ddd898a 100644 --- a/core/copyTERMUX.go +++ b/core/copyTERMUX.go @@ -3,24 +3,24 @@ package core import ( + "errors" "os/exec" "github.com/rwinkhart/go-boilerplate/back" - "github.com/rwinkhart/libmutton/global" ) // copyString copies a string to the clipboard. -func copyString(continuous bool, copySubject string) { +func copyString(continuous bool, copySubject string) error { cmd := exec.Command("termux-clipboard-set") back.WriteToStdin(cmd, copySubject) err := cmd.Run() if err != nil { - back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true) + return errors.New("unable to copy to clipboard: " + err.Error()) } - if !continuous { LaunchClipClearProcess(copySubject) } + return nil } // getClipCommands returns the commands for pasting and clearing the clipboard contents. diff --git a/core/copyUNIX.go b/core/copyUNIX.go index 5da84af..4014836 100644 --- a/core/copyUNIX.go +++ b/core/copyUNIX.go @@ -3,36 +3,36 @@ package core import ( + "errors" "os" "os/exec" "github.com/rwinkhart/go-boilerplate/back" - "github.com/rwinkhart/libmutton/global" ) // copyString copies a string to the clipboard. -func copyString(continuous bool, copySubject string) { +func copyString(continuous bool, copySubject string) error { + // determine whether to use wl-copy (Wayland) or xclip (X11) var envSet, isWayland bool // track whether environment variables are set var cmdCopy *exec.Cmd - // determine whether to use wl-copy (Wayland) or xclip (X11) if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet { cmdCopy = exec.Command("wl-copy", "-t", "text/plain") isWayland = true } else if _, envSet = os.LookupEnv("DISPLAY"); envSet { cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain") } else { - back.PrintError("Clipboard platform could not be determined", global.ErrorClipboard, true) + return errors.New("clipboard platform could not be determined") } back.WriteToStdin(cmdCopy, copySubject) err := cmdCopy.Run() if err != nil { - back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true) + return errors.New("unable to copy to clipboard: " + err.Error()) } - if !continuous { LaunchClipClearProcess(copySubject, isWayland) } + return nil } // getClipCommands returns the commands for pasting and clearing the clipboard contents. diff --git a/core/copyWIN.go b/core/copyWIN.go index 11fde29..a105d81 100644 --- a/core/copyWIN.go +++ b/core/copyWIN.go @@ -3,25 +3,23 @@ package core import ( + "errors" "fmt" "os/exec" "strings" - - "github.com/rwinkhart/go-boilerplate/back" - "github.com/rwinkhart/libmutton/global" ) // copyString copies a string to the clipboard. -func copyString(continuous bool, copySubject string) { +func copyString(continuous bool, copySubject string) error { cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''"))) err := cmd.Run() if err != nil { - back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true) + return errors.New("unable to copy to clipboard: " + err.Error()) } - if !continuous { LaunchClipClearProcess(copySubject) } + return nil } // getClipCommands returns the commands for pasting and clearing the clipboard contents. diff --git a/core/edit.go b/core/edit.go index 340839a..cdcddc2 100644 --- a/core/edit.go +++ b/core/edit.go @@ -1,23 +1,28 @@ package core import ( + "errors" + "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/libmutton/crypt" ) // GetOldEntryData decrypts and returns old entry data (with all required lines present). -func GetOldEntryData(targetLocation string, field int) []string { +func GetOldEntryData(targetLocation string, field int) ([]string, error) { // ensure targetLocation exists back.TargetIsFile(targetLocation, true, 2) // read old entry data - unencryptedEntry := crypt.DecryptFileToSlice(targetLocation) + unencryptedEntry, err := crypt.DecryptFileToSlice(targetLocation) + if err != nil { + return nil, errors.New("unable to decrypt entry: " + err.Error()) + } // return the old entry data with all required lines present if field > 0 { - return ensureSliceLength(unencryptedEntry, field) + return ensureSliceLength(unencryptedEntry, field), nil } else { - return unencryptedEntry + return unencryptedEntry, nil } } diff --git a/core/init.go b/core/init.go index a03e387..c78e033 100644 --- a/core/init.go +++ b/core/init.go @@ -23,7 +23,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][ sshKeyPath := cmp.Or(back.ExpandPathWithHome(inputCB(back.AnsiBold+"Note:"+back.AnsiReset+" Only key-based authentication is supported (keys may optionally be passphrase-protected).\n The remote server must already be in your ~"+global.PathSeparator+".ssh"+global.PathSeparator+"known_hosts file.\n\nSSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey) sshKeyIsFile, _ := back.TargetIsFile(sshKeyPath, false, 0) if !sshKeyIsFile { - return errors.New("ssh identity file not found: " + sshKeyPath) + return errors.New("SSH identity file not found: " + sshKeyPath) } // get other ssh info from user @@ -38,7 +38,10 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][ // perform operations based on collected user input //// initialize libmutton directories - oldDeviceID := global.DirInit(preserveOldConfigDir) + oldDeviceID, err := global.DirInit(preserveOldConfigDir) + if err != nil { + return errors.New("unable to initialize libmutton directories: " + err.Error()) + } //// write config file //// temporarily assign sshEntryRoot and sshIsWindows to null to pass initial device ID registration cfg.WriteConfig(append( @@ -54,7 +57,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][ // generate and register device ID sshEntryRoot, sshIsWindows, err := synccycles.DeviceIDGen(oldDeviceID) if err != nil { - return errors.New("failed to generate device ID: " + err.Error()) + return errors.New("unable to generate device ID: " + err.Error()) } cfg.WriteConfig([][3]string{{"LIBMUTTON", "sshEntryRoot", sshEntryRoot}, {"LIBMUTTON", "sshIsWindows", sshIsWindows}}, nil, true) } else { @@ -69,7 +72,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][ if len(rcwPassphrase) > 0 { err := wrappers.GenSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", rcwPassphrase) if err != nil { - return errors.New("failed to generate sanity check file: " + err.Error()) + return errors.New("unable to generate sanity check file: " + err.Error()) } } return nil diff --git a/core/utilitiesMisc.go b/core/utilitiesMisc.go index 50749de..cc90871 100644 --- a/core/utilitiesMisc.go +++ b/core/utilitiesMisc.go @@ -1,6 +1,7 @@ package core import ( + "errors" "os" "strings" @@ -10,12 +11,13 @@ import ( ) // WriteEntry writes entryData to an encrypted file at targetLocation. -func WriteEntry(targetLocation string, entryData []byte) { +func WriteEntry(targetLocation string, entryData []byte) error { encBytes := crypt.EncryptBytes(entryData) err := os.WriteFile(targetLocation, encBytes, 0600) if err != nil { - back.PrintError("Failed to write to file: "+err.Error(), back.ErrorWrite, true) + return errors.New("unable to write to file: " + err.Error()) } + return nil } // ClampTrailingWhitespace strips trailing newlines, carriage returns, and tabs from each line in a note. @@ -51,21 +53,19 @@ func ClampTrailingWhitespace(note []string) { // EntryAddPrecheck ensures the directory meant to contain a new // entry exists and that the target entry location is not already used. // Returns: statusCode (0 = success, 1 = target location already exists, 2 = containing directory is invalid). -func EntryAddPrecheck(targetLocation string) uint8 { +func EntryAddPrecheck(targetLocation string) (uint8, error) { // ensure target location does not already exist _, isAccessible := back.TargetIsFile(targetLocation, false, 0) if isAccessible { - back.PrintError("Target location already exists", global.ErrorTargetExists, false) - return 1 // inform interactive clients that the target location already exists + return 1, errors.New("target location already exists") } // ensure target containing directory exists and is a directory (not a file) containingDir := targetLocation[:strings.LastIndex(targetLocation, global.PathSeparator)] isFile, isAccessible := back.TargetIsFile(containingDir, false, 1) if isFile || !isAccessible { - back.PrintError("\""+containingDir+"\" is not a valid containing directory", back.ErrorTargetWrongType, false) - return 2 // inform interactive clients that the containing directory is invalid + return 2, errors.New("\"" + containingDir + "\" is not a valid containing directory") } - return 0 + return 0, nil } // EntryIsNotEmpty iterates through entryData and returns true if any line is not empty. diff --git a/crypt/rcw.go b/crypt/rcw.go index da61ab6..d308d40 100644 --- a/crypt/rcw.go +++ b/crypt/rcw.go @@ -1,6 +1,7 @@ package crypt import ( + "errors" "fmt" "os" "os/exec" @@ -25,26 +26,26 @@ func RCWDArgument() { } // DecryptFileToSlice decrypts an RCW wrapped file and returns the contents as a slice of (trimmed) strings. -func DecryptFileToSlice(targetLocation string) []string { +func DecryptFileToSlice(targetLocation string) ([]string, error) { // read encrypted file encBytes, err := os.ReadFile(targetLocation) if err != nil { - back.PrintError("Failed to open \""+targetLocation+"\" for decryption - "+err.Error(), back.ErrorRead, true) + return nil, errors.New("unable to open \"" + targetLocation + "\" for decryption: " + err.Error()) } // decrypt data using RCW daemon passphrase := launchRCWDProcess() if passphrase == nil { // if daemon is already running, use it to decrypt the data - return strings.Split(string(daemon.GetDec(encBytes)), "\n") + return strings.Split(string(daemon.GetDec(encBytes)), "\n"), nil } // if the daemon is not already running, use wrappers.Decrypt // directly to avoid waiting for socket file creation decBytes, err := wrappers.Decrypt(encBytes, passphrase) if err != nil { - back.PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), global.ErrorDecryption, true) + return nil, errors.New("unable to decrypt \"" + targetLocation + "\": " + err.Error()) } - return strings.Split(string(decBytes), "\n") + return strings.Split(string(decBytes), "\n"), nil } // EncryptBytes encrypts a byte slice using RCW and returns the encrypted data. diff --git a/global/deviceIDs.go b/global/deviceIDs.go index 6990c59..5ad92d1 100644 --- a/global/deviceIDs.go +++ b/global/deviceIDs.go @@ -1,36 +1,34 @@ package global import ( + "errors" "io/fs" "os" - - "github.com/rwinkhart/go-boilerplate/back" ) // GetOldDeviceID returns the current device ID or // FSMisc if there is no device ID (e.g. first run). -func GetCurrentDeviceID() string { - deviceIDList := GenDeviceIDList(false) // errorOnFail is false so that nil is received when the devices directory does not exist +func GetCurrentDeviceID() (string, error) { + deviceIDList, err := GenDeviceIDList() + if err != nil { + return "", errors.New("unable to generate device ID list: " + err.Error()) + } var deviceID string if len(deviceIDList) > 0 { deviceID = (deviceIDList)[0].Name() } else { deviceID = FSMisc // indicates to server that no device ID is being replaced } - return deviceID + return deviceID, nil } // GenDeviceIDList returns a slice of all registered device IDs. // Requires: errorOnFail (set to true to throw an error if the devices directory cannot be read/does not exist) -func GenDeviceIDList(errorOnFail bool) []fs.DirEntry { +func GenDeviceIDList() ([]fs.DirEntry, error) { // create a slice of all registered devices deviceIDList, err := os.ReadDir(ConfigDir + PathSeparator + "devices") if err != nil { - if errorOnFail { - back.PrintError("Failed to read the devices directory: "+err.Error(), back.ErrorRead, true) - } else { - return nil // a nil return value indicates that the devices directory could not be read/does not exist - } + return nil, errors.New("unable to read the devices directory: " + err.Error()) } - return deviceIDList + return deviceIDList, nil } diff --git a/global/init.go b/global/init.go index 0ed0b1f..54db209 100644 --- a/global/init.go +++ b/global/init.go @@ -1,6 +1,7 @@ package global import ( + "errors" "os" "github.com/rwinkhart/go-boilerplate/back" @@ -8,15 +9,18 @@ import ( // DirInit creates the libmutton directories. // Returns: oldDeviceID (from before the directory reset; will be FSMisc if there is no pre-existing ID). -func DirInit(preserveOldConfigDir bool) string { +func DirInit(preserveOldConfigDir bool) (string, error) { // create EntryRoot err := os.MkdirAll(EntryRoot, 0700) if err != nil { - back.PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), back.ErrorWrite, true) + return "", errors.New("unable to create \"" + EntryRoot + "\": " + err.Error()) } // get old device ID before its potential removal - oldDeviceID := GetCurrentDeviceID() + oldDeviceID, err := GetCurrentDeviceID() + if err != nil { + return "", errors.New("unable to get current device ID: " + err.Error()) + } // remove existing config directory (if it exists and not in append mode) if !preserveOldConfigDir { @@ -24,7 +28,7 @@ func DirInit(preserveOldConfigDir bool) string { if isAccessible { err = os.RemoveAll(ConfigDir) if err != nil { - back.PrintError("Failed to remove existing config directory: "+err.Error(), back.ErrorWrite, true) + return "", errors.New("unable to remove existing config directory: " + err.Error()) } } } @@ -32,8 +36,8 @@ func DirInit(preserveOldConfigDir bool) string { // create config directory w/devices subdirectory err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700) if err != nil { - back.PrintError("Failed to create \""+ConfigDir+"\": "+err.Error(), back.ErrorWrite, true) + return "", errors.New("unable to create \"" + ConfigDir + "\": " + err.Error()) } - return oldDeviceID + return oldDeviceID, nil } diff --git a/syncclient/client.go b/syncclient/client.go index e49fa8c..0cb7012 100644 --- a/syncclient/client.go +++ b/syncclient/client.go @@ -99,11 +99,11 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool, error) { } // GetSSHOutput runs a command over SSH and returns the output as a string. -func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string { +func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) (string, error) { // create a session sshSession, err := sshClient.NewSession() if err != nil { - back.PrintError("Sync failed - Unable to establish SSH session: "+err.Error(), global.ErrorServerConnection, true) + return "", errors.New("unable to establish SSH session: " + err.Error()) } // provide stdin data for session @@ -113,40 +113,46 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string { var output []byte output, err = sshSession.CombinedOutput(cmd) if err != nil { - back.PrintError("Sync failed - Unable to run SSH command: "+err.Error(), global.ErrorSyncProcess, true) + return "", errors.New("unable to run SSH command: " + err.Error()) } // convert the output to a string and remove leading/trailing whitespace outputString := string(output) outputString = strings.TrimSpace(outputString) - return outputString + return outputString, nil } // getRemoteDataFromClient returns a map of remote entries to their modification times, a list of remote folders, a list of queued deletions, and the current server&client times as UNIX timestamps. -func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string]int64, []string, []string, int64, int64) { +func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string]int64, []string, []string, int64, int64, error) { // get remote output over SSH - deviceIDList := global.GenDeviceIDList(true) + deviceIDList, err := global.GenDeviceIDList() + if err != nil { + return nil, nil, nil, 0, 0, err + } if len(deviceIDList) == 0 { if manualSync { - back.PrintError("Sync failed - No device ID found", back.ErrorTargetNotFound, true) + return nil, nil, nil, 0, 0, errors.New("no device ID found") } else { back.Exit(0) // exit silently if the sync job was called automatically, as the user may just be in offline mode } } clientTime := time.Now().Unix() // get client time now to avoid accuracy issues caused by unpredictable sync time - output := GetSSHOutput(sshClient, "libmuttonserver fetch", (deviceIDList)[0].Name()) + output, err := GetSSHOutput(sshClient, "libmuttonserver fetch", (deviceIDList)[0].Name()) + if err != nil { + return nil, nil, nil, 0, 0, errors.New("unable to run remote command: " + err.Error()) + } // split output into slice based on occurrences of FSSpace outputSlice := strings.Split(output, global.FSSpace) // parse output/re-form lists if len(outputSlice) != 5 { // ensure information from server is complete - back.PrintError("Sync failed - Unable to fetch remote data; server returned an unexpected response", global.ErrorSyncProcess, true) + return nil, nil, nil, 0, 0, errors.New("unable to run remote command; server returned an unexpected response") } serverTime, err := strconv.ParseInt(outputSlice[0], 10, 64) if err != nil { - back.PrintError("Sync failed - Unable to parse server time: "+err.Error(), back.ErrorRead, true) + return nil, nil, nil, 0, 0, errors.New("unable to parse server time: " + err.Error()) } entries := strings.Split(outputSlice[1], global.FSMisc)[1:] modsStrings := strings.Split(outputSlice[2], global.FSMisc)[1:] @@ -159,7 +165,7 @@ func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string for _, modString := range modsStrings { mod, err = strconv.ParseInt(modString, 10, 64) if err != nil { - back.PrintError("Sync failed - Unable to parse mod time: "+err.Error(), back.ErrorRead, true) + return nil, nil, nil, 0, 0, errors.New("unable to parse mod time: " + err.Error()) } mods = append(mods, mod) } @@ -170,13 +176,16 @@ func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string entryModMap[entry] = mods[i] } - return entryModMap, folders, deletions, serverTime, clientTime + return entryModMap, folders, deletions, serverTime, clientTime, nil } // getLocalData returns a map of local entries to their modification times. -func getLocalData() map[string]int64 { +func getLocalData() (map[string]int64, error) { // get a list of all entries - entries, _ := synccommon.WalkEntryDir() + entries, _, err := synccommon.WalkEntryDir() + if err != nil { + return nil, err + } // get a list of all entry modification times modList := synccommon.GetModTimes(entries) @@ -188,7 +197,7 @@ func getLocalData() map[string]int64 { } // return the lists - return entryModMap + return entryModMap, nil } // targetLocationFormatSFTP formats the target location to match the remote server's entry directory and path separator. @@ -201,17 +210,14 @@ func targetLocationFormatSFTP(targetName, serverEntryRoot string, serverIsWindow } // sftpSync takes two slices of entries (one for downloads and one for uploads) and syncs them between the client and server using SFTP. -func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, downloadList, uploadList []string) { +func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, downloadList, uploadList []string) error { // create an SFTP client from sshClient sftpClient, err := sftp.NewClient(sshClient) if err != nil { - back.PrintError("Sync failed - Unable to establish SFTP session: "+err.Error(), global.ErrorServerConnection, true) + return errors.New("unable to establish SFTP session: " + err.Error()) } defer func(sftpClient *sftp.Client) { - err = sftpClient.Close() - if err != nil { - back.PrintError("Sync failed - Unable to close SFTP client: "+err.Error(), global.ErrorServerConnection, true) - } + _ = sftpClient.Close() }(sftpClient) // iterate over the download list @@ -228,7 +234,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow var fileInfo os.FileInfo fileInfo, err = sftpClient.Stat(remoteEntryFullPath) if err != nil { - back.PrintError("Sync failed - Unable to get remote file info (mod time): "+err.Error(), back.ErrorRead, true) + return errors.New("unable to get remote file info (mod time): " + err.Error()) } modTime := fileInfo.ModTime() @@ -236,7 +242,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow var remoteFile *sftp.File remoteFile, err = sftpClient.Open(remoteEntryFullPath) if err != nil { - back.PrintError("Sync failed - Unable to open remote file: "+err.Error(), back.ErrorRead, true) + return errors.New("unable to open remote file: " + err.Error()) } // store path to local entry @@ -246,13 +252,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow var localFile *os.File localFile, err = os.OpenFile(localEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) if err != nil { - back.PrintError("Sync failed - Unable to create local file: "+err.Error(), back.ErrorWrite, true) + return errors.New("unable to create local file: " + err.Error()) } // download the file _, err = remoteFile.WriteTo(localFile) if err != nil { - back.PrintError("Sync failed - Unable to download remote file: "+err.Error(), global.ErrorSyncProcess, true) + return errors.New("unable to download remote file: " + err.Error()) } // close the files @@ -281,7 +287,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow var fileInfo os.FileInfo fileInfo, err = os.Stat(localEntryFullPath) if err != nil { - back.PrintError("Sync failed - Unable to get local file info (mod time): "+err.Error(), back.ErrorRead, true) + return errors.New("unable to get local file info (mod time): " + err.Error()) } modTime := fileInfo.ModTime() @@ -289,7 +295,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow var localFile *os.File localFile, err = os.Open(localEntryFullPath) if err != nil { - back.PrintError("Sync failed - Unable to open local file: "+err.Error(), back.ErrorRead, true) + return errors.New("unable to open local file: " + err.Error()) } // store path to remote entry @@ -299,13 +305,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow var remoteFile *sftp.File remoteFile, err = sftpClient.OpenFile(remoteEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY) if err != nil { - back.PrintError("Sync failed - Unable to create remote file ("+remoteEntryFullPath+"): "+err.Error(), back.ErrorWrite, true) + return errors.New("unable to create remote file: " + err.Error()) } // upload the file _, err = localFile.WriteTo(remoteFile) if err != nil { - back.PrintError("Sync failed - Unable to upload local file: "+err.Error(), global.ErrorSyncProcess, true) + return errors.New("unable to upload local file: " + err.Error()) } // close the files @@ -315,7 +321,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow // set permissions on remote file err = sftpClient.Chmod(remoteEntryFullPath, 0600) if err != nil { - back.PrintError("Sync failed - Unable to set permissions on remote file: "+err.Error(), global.ErrorSyncProcess, true) + return errors.New("unable to set permissions on remote file: " + err.Error()) } // set the modification time of the remote file to match the value saved from the local file (from before the upload) @@ -325,6 +331,8 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow if filesTransferred { fmt.Println() // add a gap between upload and sync complete messages } + + return nil } // syncLists determines which entries need to be downloaded and uploaded for synchronizations and calls sftpSync with this information. @@ -377,24 +385,24 @@ func syncLists(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows, timeSyn } // deletionSync removes entries from the client that have been deleted on the server (multi-client deletion). -func deletionSync(deletions []string) { +func deletionSync(deletions []string) error { var filesDeleted bool for _, deletion := range deletions { filesDeleted = true // set a flag to indicate that files have been deleted (used to determine whether to print a gap between deletion and other messages) fmt.Println(synccommon.AnsiDelete+deletion+back.AnsiReset, "has been sheared, removing locally (if it exists)") err := os.RemoveAll(global.TargetLocationFormat(deletion)) if err != nil { - back.PrintError("Sync failed - Failed to shear "+deletion+" locally: "+err.Error(), back.ErrorWrite, true) + return errors.New("unable to shear " + deletion + " locally: " + err.Error()) } } - if filesDeleted { fmt.Println() // add a gap between deletion and other messages } + return nil } // folderSync creates folders on the client (from the given list of folder names). -func folderSync(folders []string) { +func folderSync(folders []string) error { for _, folder := range folders { // store the full local path of the folder folderFullPath := global.TargetLocationFormat(folder) @@ -405,35 +413,33 @@ func folderSync(folders []string) { if !isFile && !isAccessible { err := os.MkdirAll(folderFullPath, 0700) if err != nil { - back.PrintError("Sync failed - Failed to create folder ("+folder+"): "+err.Error(), back.ErrorWrite, true) + return errors.New("unable to create folder (" + folder + "): " + err.Error()) } } else if isFile { - back.PrintError("Sync failed - Failed to create folder ("+folder+") - A file with the same name already exists", global.ErrorTargetExists, true) + return errors.New("unable to create folder (" + folder + "): a file with the same name already exists") } } + return nil } // RunJob runs the SSH sync job. // Setting manualSync to true will throw errors if sync is not configured (online mode is assumed). // Setting returnLists to true will return the deletions, downloads, and uploads lists for use by the client. -func RunJob(manualSync, returnLists bool) [3][]string { +func RunJob(manualSync, returnLists bool) ([3][]string, error) { // get SSH client to re-use throughout the sync process sshClient, sshEntryRoot, sshIsWindows, err := GetSSHClient(manualSync) if err != nil { - back.PrintError("sync failed - unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true) - } - if sshClient == nil { // indicate SSH dialing failure for interactive clients - return [3][]string{nil, nil, nil} + return [3][]string{nil, nil, nil}, errors.New("unable to connect to SSH client: " + err.Error()) } defer func(sshClient *ssh.Client) { - err := sshClient.Close() - if err != nil { - back.PrintError("sync failed - unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true) - } + _ = sshClient.Close() }(sshClient) // fetch remote lists - remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime := getRemoteDataFromClient(sshClient, manualSync) + remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime, err := getRemoteDataFromClient(sshClient, manualSync) + if err != nil { + return [3][]string{nil, nil, nil}, errors.New("unable to fetch remote data: " + err.Error()) + } // sync deletions deletionSync(deletions) @@ -442,7 +448,10 @@ func RunJob(manualSync, returnLists bool) [3][]string { folderSync(remoteFolders) // fetch local lists - localEntryModMap := getLocalData() + localEntryModMap, err := getLocalData() + if err != nil { + return [3][]string{nil, nil, nil}, errors.New("unable to fetch local entry data: " + err.Error()) + } // prior to syncing lists, ensure the client and server clocks are synced within 45 seconds var timeSynced = true @@ -457,9 +466,9 @@ func RunJob(manualSync, returnLists bool) [3][]string { if returnLists { lists = syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap) lists[0] = deletions - return lists + return lists, nil } syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, false, localEntryModMap, remoteEntryModMap) - back.Exit(0) // exit program if running non-interactively - return lists // dummy return for when not returning lists + back.Exit(0) // exit program if running non-interactively + return lists, nil // dummy return for when not returning lists } diff --git a/syncclient/oneOff.go b/syncclient/oneOff.go index 7be62ca..4123a7e 100644 --- a/syncclient/oneOff.go +++ b/syncclient/oneOff.go @@ -1,6 +1,7 @@ package syncclient import ( + "errors" "strings" "github.com/rwinkhart/go-boilerplate/back" @@ -10,14 +11,17 @@ import ( // ShearRemoteFromClient removes the target file or directory from the local system and calls the server to remove it remotely and add it to the deletions list. // It can safely be called in offline mode, as well, so this is the intended interface for shearing (ShearLocal should only be used directly by the server binary). -func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) { - deviceID, isDir := synccommon.ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client +func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) error { + deviceID, isDir, err := synccommon.ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client + if err != nil { + return errors.New("unable to shear target locally: " + err.Error()) + } if !forceOffline && deviceID != "" { // ensure a device ID exists (online mode) // create an SSH client; manualSync is false in case a device ID exists but SSH is not configured sshClient, _, _, err := GetSSHClient(false) if err != nil { - back.PrintError("Sync failed - Unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true) + return errors.New("unable to connect to SSH client: " + err.Error()) } // ensure targetLocationIncomplete ends with a slash if it is a directory (for clarity in shear message) @@ -31,24 +35,28 @@ func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) { // close the SSH client err = sshClient.Close() if err != nil { - back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true) + return errors.New("unable to close SSH client: " + err.Error()) } } back.Exit(0) // sync is not required after shearing since the target has already been removed from the local system + return nil } // RenameRemoteFromClient renames oldLocationIncomplete to newLocationIncomplete on the local system and calls the server to perform the rename remotely and add the old target to the deletions list. // It can safely be called in offline mode, as well, so this is the intended interface for renaming (RenameLocal should only be used directly by the server binary). -func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string, forceOffline bool) { +func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string, forceOffline bool) error { synccommon.RenameLocal(oldLocationIncomplete, newLocationIncomplete, false) // move the target on the local system - deviceIDList := global.GenDeviceIDList(true) + deviceIDList, err := global.GenDeviceIDList() + if err != nil { + return errors.New("unable to generate device ID list: " + err.Error()) + } if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode) // create an SSH client; manualSync is false in case a device ID exists but SSH is not configured sshClient, _, _, err := GetSSHClient(false) if err != nil { - back.PrintError("Sync failed - Unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true) + return errors.New("unable to connect to SSH client: " + err.Error()) } // call the server to move the target on the remote system and add the old target to the deletions list @@ -60,24 +68,28 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string, // close the SSH client err = sshClient.Close() if err != nil { - back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true) + return errors.New("unable to close SSH client: " + err.Error()) } } back.Exit(0) + return nil } // AddFolderRemoteFromClient creates a new entry-containing directory on the local system and calls the server to create the folder remotely. // It can safely be called in offline mode, as well, so this is the intended interface for adding folders (AddFolderLocal should only be used directly by the server binary). -func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline bool) { +func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline bool) error { synccommon.AddFolderLocal(targetLocationIncomplete) // add the folder on the local system - deviceIDList := global.GenDeviceIDList(true) + deviceIDList, err := global.GenDeviceIDList() + if err != nil { + return errors.New("unable to generate device ID list: " + err.Error()) + } if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode) // create an SSH client; manualSync is false in case a device ID exists but SSH is not configured sshClient, _, _, err := GetSSHClient(false) if err != nil { - back.PrintError("Sync failed - Unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true) + return errors.New("unable to connect to SSH client: " + err.Error()) } // call the server to create the folder remotely @@ -86,9 +98,10 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline boo // close the SSH client err = sshClient.Close() if err != nil { - back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true) + return errors.New("unable to close SSH client: " + err.Error()) } } back.Exit(0) + return nil } diff --git a/synccommon/common.go b/synccommon/common.go index e1fe430..33fa410 100644 --- a/synccommon/common.go +++ b/synccommon/common.go @@ -1,6 +1,7 @@ package synccommon import ( + "errors" "fmt" "os" "strings" @@ -34,14 +35,17 @@ func GetModTimes(entryList []string) []int64 { // isDir (only on client; for use in ShearRemoteFromClient). // If the local system is a server, it will also add the target to the deletions list for all clients (except the requesting client). // This function should only be used directly by the server binary. -func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool) { +func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool, error) { // determine if running on a server var onServer bool if clientDeviceID != "" { onServer = true } - deviceIDList := global.GenDeviceIDList(true) + deviceIDList, err := global.GenDeviceIDList() + if err != nil { + return "", false, errors.New("unable to generate device ID list: " + err.Error()) + } // add the sheared target (incomplete, vanity) to the deletions list (if running on a server) if onServer { @@ -64,22 +68,22 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool) if !onServer { // error if target does not exist on client, needed because os.RemoveAll does not return an error if target does not exist isFile, _ = back.TargetIsFile(targetLocationComplete, true, 0) } - err := os.RemoveAll(targetLocationComplete) + err = os.RemoveAll(targetLocationComplete) if err != nil { - back.PrintError("Failed to remove local target: "+err.Error(), back.ErrorWrite, true) + return "", false, errors.New("unable to remove local target: " + err.Error()) } if !onServer && len(deviceIDList) > 0 { // return the device ID if running on the client and a device ID exists (online mode) - return (deviceIDList)[0].Name(), !isFile + return (deviceIDList)[0].Name(), !isFile, nil } - return "", true + return "", true, nil // do not exit program, as this function is used as part of ShearRemoteFromClient } // RenameLocal renames oldLocationIncomplete to newLocationIncomplete on the local system. // This function should only be used directly by the server binary. -func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldLocationExists bool) { +func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldLocationExists bool) error { // get full paths for both locations oldLocation := global.TargetLocationFormat(oldLocationIncomplete) newLocation := global.TargetLocationFormat(newLocationIncomplete) @@ -91,21 +95,23 @@ func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldL // ensure newLocation does not exist _, isAccessible := back.TargetIsFile(newLocation, false, 0) if isAccessible { - back.PrintError("\""+newLocation+"\" already exists", global.ErrorTargetExists, true) + return errors.New("target already exists: " + newLocation) } // rename oldLocation to newLocation err := os.Rename(oldLocation, newLocation) if err != nil { - back.PrintError("Failed to rename - Does the target containing directory exist?", back.ErrorTargetNotFound, true) + return errors.New("unable to rename: " + err.Error()) } + return nil + // do not exit program, as this function is used as part of RenameRemoteFromClient } // AddFolderLocal creates a new entry-containing directory on the local system. // This function should only be used directly by the server binary. -func AddFolderLocal(targetLocationIncomplete string) { +func AddFolderLocal(targetLocationIncomplete string) error { // get the full targetLocation path and create the target targetLocationComplete := global.TargetLocationFormat(targetLocationIncomplete) err := os.Mkdir(targetLocationComplete, 0700) @@ -113,9 +119,11 @@ func AddFolderLocal(targetLocationIncomplete string) { if os.IsExist(err) { fmt.Println(AnsiUpload + "Directory already exists - libmutton will still ensure it exists on the server") } else { - back.PrintError("Failed to create directory: "+err.Error(), back.ErrorWrite, true) + return errors.New("unable to create directory: " + err.Error()) } } + return nil + // do not exit program, as this function is used as part of AddFolderRemoteFromClient } diff --git a/synccommon/walkUNIX.go b/synccommon/walkUNIX.go index 510f4e5..7937738 100644 --- a/synccommon/walkUNIX.go +++ b/synccommon/walkUNIX.go @@ -3,31 +3,31 @@ package synccommon import ( + "errors" "io/fs" "os" "path/filepath" - "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/libmutton/global" ) // WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists). // Regardless of platform, all paths are stored with forward slashes (UNIX-style). -func WalkEntryDir() ([]string, []string) { +func WalkEntryDir() ([]string, []string, error) { // define file/directory containing slices so that they may be accessed by the anonymous WalkDir function var fileList []string var dirList []string // walk entry directory - _ = filepath.WalkDir(global.EntryRoot, + err := filepath.WalkDir(global.EntryRoot, func(fullPath string, entry fs.DirEntry, err error) error { // check for errors encountered while walking directory if err != nil { if os.IsNotExist(err) { - back.PrintError("The entry directory does not exist - Initialize libmutton to create it", back.ErrorOther, true) + return errors.New("entry directory does not exist; initialize libmutton to create it") } else { - back.PrintError("An unexpected error occurred while generating the entry list: "+err.Error(), back.ErrorOther, true) + return errors.New("an unexpected error occurred while generating the entry list: " + err.Error()) } } @@ -43,6 +43,8 @@ func WalkEntryDir() ([]string, []string) { return nil }) - - return fileList, dirList + if err != nil { + return nil, nil, err + } + return fileList, dirList, nil } diff --git a/synccommon/walkWIN.go b/synccommon/walkWIN.go index 6f61666..52b6667 100644 --- a/synccommon/walkWIN.go +++ b/synccommon/walkWIN.go @@ -3,32 +3,32 @@ package synccommon import ( + "errors" "io/fs" "os" "path/filepath" "strings" - "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/libmutton/global" ) // WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists). // Regardless of platform, all paths are stored with forward slashes (UNIX-style). -func WalkEntryDir() ([]string, []string) { +func WalkEntryDir() ([]string, []string, error) { // define file/directory containing slices so that they may be accessed by the anonymous WalkDir function var fileList []string var dirList []string // walk entry directory - _ = filepath.WalkDir(global.EntryRoot, + err := filepath.WalkDir(global.EntryRoot, func(fullPath string, entry fs.DirEntry, err error) error { // check for errors encountered while walking directory if err != nil { if os.IsNotExist(err) { - back.PrintError("The entry directory does not exist - Initialize libmutton to create it", back.ErrorOther, true) + return errors.New("entry directory does not exist; initialize libmutton to create it") } else { - back.PrintError("An unexpected error occurred while generating the entry list: "+err.Error(), back.ErrorOther, true) + return errors.New("an unexpected error occurred while generating the entry list: " + err.Error()) } } @@ -44,6 +44,8 @@ func WalkEntryDir() ([]string, []string) { return nil }) - - return fileList, dirList + if err != nil { + return nil, nil, err + } + return fileList, dirList, nil } diff --git a/synccycles/init.go b/synccycles/init.go index dddbea5..1958f0d 100644 --- a/synccycles/init.go +++ b/synccycles/init.go @@ -25,14 +25,14 @@ func DeviceIDGen(oldDeviceID string) (string, string, error) { // create new device ID file (locally) fileToClose, err := os.OpenFile(global.ConfigDir+global.PathSeparator+"devices"+global.PathSeparator+newDeviceID, os.O_CREATE|os.O_WRONLY, 0600) if err != nil { - return "", "", errors.New("failed to create local device ID file: " + err.Error()) + return "", "", errors.New("unable to create local device ID file: " + err.Error()) } _ = fileToClose.Close() // error ignored; if the file could be created, it can probably be closed // remove old device ID file (locally; may not exist) err = os.RemoveAll(global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + oldDeviceID) if err != nil { - return "", "", errors.New("failed to remove old device ID file (locally): " + err.Error()) + return "", "", errors.New("unable to remove old device ID file (locally): " + err.Error()) } // register new device ID with server and fetch remote EntryRoot and OS type @@ -40,12 +40,16 @@ func DeviceIDGen(oldDeviceID string) (string, string, error) { // manualSync is true so the user is alerted if device ID registration fails sshClient, _, _, err := syncclient.GetSSHClient(true) if err != nil { - return "", "", errors.New("device ID gen failed - unable to connect to SSH client: " + err.Error()) + return "", "", errors.New("unable to connect to SSH client: " + err.Error()) } - sshEntryRootSSHIsWindows := strings.Split(syncclient.GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), global.FSSpace) + output, err := syncclient.GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID) + if err != nil { + return "", "", errors.New("unable to register device ID with server: " + err.Error()) + } + sshEntryRootSSHIsWindows := strings.Split(output, global.FSSpace) err = sshClient.Close() if err != nil { - return "", "", errors.New("device ID gen failed - unable to close SSH client: " + err.Error()) + return "", "", errors.New("unable to close SSH client: " + err.Error()) } return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1], nil diff --git a/syncserver/server.go b/syncserver/server.go index e79672a..ab32c78 100644 --- a/syncserver/server.go +++ b/syncserver/server.go @@ -1,12 +1,12 @@ package syncserver import ( + "errors" "fmt" "os" "strings" "time" - "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/libmutton/global" "github.com/rwinkhart/libmutton/synccommon" ) @@ -14,12 +14,15 @@ import ( // GetRemoteDataFromServer prints to stdout the remote entries, mod times, folders, and deletions. // Lists in output are separated by FSSpace. // Output is meant to be captured over SSH for interpretation by the client. -func GetRemoteDataFromServer(clientDeviceID string) { - entryList, dirList := synccommon.WalkEntryDir() +func GetRemoteDataFromServer(clientDeviceID string) error { + entryList, dirList, err := synccommon.WalkEntryDir() + if err != nil { + return errors.New("unable to walk the entry directory: " + err.Error()) + } modList := synccommon.GetModTimes(entryList) deletionsList, err := os.ReadDir(global.ConfigDir + global.PathSeparator + "deletions") if err != nil { - back.PrintError("Failed to read the deletions directory: "+err.Error(), back.ErrorRead, true) + return errors.New("unable to read the deletions directory: " + err.Error()) } // print the current UNIX timestamp to stdout @@ -57,4 +60,5 @@ func GetRemoteDataFromServer(clientDeviceID string) { _ = os.Remove(global.ConfigDir + global.PathSeparator + "deletions" + global.PathSeparator + deletion.Name()) // error ignored; function not run from a user-facing argument and thus the error would not be visible } } + return nil }