Return errors, rather than printing them

This commit is contained in:
2025-05-30 01:08:43 +00:00
parent 74b8261bc8
commit 40f0f35f45
21 changed files with 253 additions and 183 deletions
+59 -50
View File
@@ -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
}
+25 -12
View File
@@ -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
}