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
+18 -11
View File
@@ -1,6 +1,7 @@
package cfg package cfg
import ( import (
"errors"
"fmt" "fmt"
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
@@ -10,12 +11,12 @@ import (
// loadConfig loads the libmutton.ini file and returns the configuration. // loadConfig loads the libmutton.ini file and returns the configuration.
// It is a utility function for ParseConfig and WriteConfig; do not call directly. // 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) cfg, err := ini.Load(global.ConfigPath)
if err != nil { 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. // 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). // error (nil if no error occurred, otherwise an error using the generated or provided message).
func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]string, error) { func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]string, error) {
var err error var err error
cfg := loadConfig() cfg, err := loadConfig()
if err != nil {
return nil, err
}
var config []string var config []string
@@ -36,14 +40,12 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin
if value == "" { if value == "" {
switch missingValueError { switch missingValueError {
case "": 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": case "0":
back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
default: default:
err = fmt.Errorf("%s", missingValueError) 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 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), // 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), // 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). // 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 cfg *ini.File
var err error
if append { if append {
// load existing ini file // load existing ini file
cfg = loadConfig() cfg, err = loadConfig()
if err != nil {
return errors.New("unable to load existing libmutton.ini: " + err.Error())
}
} else { } else {
// create empty ini container // create empty ini container
cfg = ini.Empty() cfg = ini.Empty()
@@ -93,8 +99,9 @@ func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool
// save to libmutton.ini // save to libmutton.ini
setUmask(0077) // only give permissions to owner setUmask(0077) // only give permissions to owner
err := cfg.SaveTo(global.ConfigPath) err = cfg.SaveTo(global.ConfigPath)
if err != nil { 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
} }
+12 -7
View File
@@ -3,30 +3,31 @@
package core package core
import ( import (
"errors"
"strings" "strings"
"time" "time"
"github.com/rwinkhart/go-boilerplate/back" "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. // 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. // assignedContents can be omitted to clear the clipboard immediately and unconditionally.
func clipClearProcess(assignedContents string) { func clipClearProcess(assignedContents string) error {
cmdPaste, cmdClear := getClipCommands() cmdPaste, cmdClear := getClipCommands()
clearClipboard := func() { clearClipboard := func() error {
err := cmdClear.Run() err := cmdClear.Run()
if err != nil { if err != nil {
back.PrintError("Failed to clear clipboard", global.ErrorClipboard, true) return errors.New("unable to clear clipboard")
} }
back.Exit(0) back.Exit(0)
return nil
} }
// if assignedContents is empty, clear the clipboard immediately and unconditionally // if assignedContents is empty, clear the clipboard immediately and unconditionally
if assignedContents == "" { if assignedContents == "" {
clearClipboard() clearClipboard()
return return nil
} }
// wait 30 seconds before checking clipboard contents // wait 30 seconds before checking clipboard contents
@@ -34,10 +35,14 @@ func clipClearProcess(assignedContents string) {
newContents, err := cmdPaste.Output() newContents, err := cmdPaste.Output()
if err != nil { 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") { if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
clearClipboard() err := clearClipboard()
if err != nil {
return err
}
} }
return nil
} }
+17 -8
View File
@@ -1,6 +1,7 @@
package core package core
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@@ -13,10 +14,13 @@ import (
) )
// CopyArgument copies a field from an entry to the clipboard. // 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 { 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 var copySubject string // will store data to be copied
// ensure field exists in entry // ensure field exists in entry
@@ -24,7 +28,7 @@ func CopyArgument(targetLocation string, field int) {
// ensure field is not empty // ensure field is not empty
if decryptedEntry[field] == "" { if decryptedEntry[field] == "" {
back.PrintError("Field is empty", back.ErrorTargetNotFound, true) return errors.New("field is empty")
} }
if field != 2 { if field != 2 {
@@ -44,18 +48,23 @@ func CopyArgument(targetLocation string, field int) {
for { // keep token copied to clipboard, refresh on 30-second intervals for { // keep token copied to clipboard, refresh on 30-second intervals
currentTime := time.Now() 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 // sleep until next 30-second interval
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second) time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
} }
} }
} else { } 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 // copy field to clipboard, launch clipboard clearing process
copyString(false, copySubject) copyString(false, copySubject)
} }
return nil
} }
// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess. // 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). // 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 totpToken string
var err error var err error
@@ -79,8 +88,8 @@ func GenTOTP(secret string, time time.Time, forSteam bool) string {
} }
if err != nil { 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
} }
+4 -4
View File
@@ -3,24 +3,24 @@
package core package core
import ( import (
"errors"
"os/exec" "os/exec"
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/global"
) )
// copyString copies a string to the clipboard. // copyString copies a string to the clipboard.
func copyString(continuous bool, copySubject string) { func copyString(continuous bool, copySubject string) error {
cmd := exec.Command("pbcopy") cmd := exec.Command("pbcopy")
back.WriteToStdin(cmd, copySubject) back.WriteToStdin(cmd, copySubject)
err := cmd.Run() err := cmd.Run()
if err != nil { 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 { if !continuous {
LaunchClipClearProcess(copySubject) LaunchClipClearProcess(copySubject)
} }
return nil
} }
// getClipCommands returns the commands for pasting and clearing the clipboard contents. // getClipCommands returns the commands for pasting and clearing the clipboard contents.
-2
View File
@@ -6,8 +6,6 @@ import (
"golang.design/x/clipboard" "golang.design/x/clipboard"
) )
// TODO Investigate background clipboard clearing and on-app-close clipboard clearing for Android
// copyString copies a string to the clipboard. // copyString copies a string to the clipboard.
func copyString(continuous bool, copySubject string) { func copyString(continuous bool, copySubject string) {
clipboard.Write(clipboard.FmtText, []byte(copySubject)) clipboard.Write(clipboard.FmtText, []byte(copySubject))
+4 -4
View File
@@ -3,24 +3,24 @@
package core package core
import ( import (
"errors"
"os/exec" "os/exec"
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/global"
) )
// copyString copies a string to the clipboard. // 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") cmd := exec.Command("termux-clipboard-set")
back.WriteToStdin(cmd, copySubject) back.WriteToStdin(cmd, copySubject)
err := cmd.Run() err := cmd.Run()
if err != nil { 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 { if !continuous {
LaunchClipClearProcess(copySubject) LaunchClipClearProcess(copySubject)
} }
return nil
} }
// getClipCommands returns the commands for pasting and clearing the clipboard contents. // getClipCommands returns the commands for pasting and clearing the clipboard contents.
+6 -6
View File
@@ -3,36 +3,36 @@
package core package core
import ( import (
"errors"
"os" "os"
"os/exec" "os/exec"
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/global"
) )
// copyString copies a string to the clipboard. // 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 envSet, isWayland bool // track whether environment variables are set
var cmdCopy *exec.Cmd var cmdCopy *exec.Cmd
// determine whether to use wl-copy (Wayland) or xclip (X11)
if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet { if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet {
cmdCopy = exec.Command("wl-copy", "-t", "text/plain") cmdCopy = exec.Command("wl-copy", "-t", "text/plain")
isWayland = true isWayland = true
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet { } else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain") cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain")
} else { } 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) back.WriteToStdin(cmdCopy, copySubject)
err := cmdCopy.Run() err := cmdCopy.Run()
if err != nil { 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 { if !continuous {
LaunchClipClearProcess(copySubject, isWayland) LaunchClipClearProcess(copySubject, isWayland)
} }
return nil
} }
// getClipCommands returns the commands for pasting and clearing the clipboard contents. // getClipCommands returns the commands for pasting and clearing the clipboard contents.
+4 -6
View File
@@ -3,25 +3,23 @@
package core package core
import ( import (
"errors"
"fmt" "fmt"
"os/exec" "os/exec"
"strings" "strings"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/global"
) )
// copyString copies a string to the clipboard. // 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, "'", "''"))) cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
err := cmd.Run() err := cmd.Run()
if err != nil { 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 { if !continuous {
LaunchClipClearProcess(copySubject) LaunchClipClearProcess(copySubject)
} }
return nil
} }
// getClipCommands returns the commands for pasting and clearing the clipboard contents. // getClipCommands returns the commands for pasting and clearing the clipboard contents.
+9 -4
View File
@@ -1,23 +1,28 @@
package core package core
import ( import (
"errors"
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/crypt" "github.com/rwinkhart/libmutton/crypt"
) )
// GetOldEntryData decrypts and returns old entry data (with all required lines present). // 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 // ensure targetLocation exists
back.TargetIsFile(targetLocation, true, 2) back.TargetIsFile(targetLocation, true, 2)
// read old entry data // 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 // return the old entry data with all required lines present
if field > 0 { if field > 0 {
return ensureSliceLength(unencryptedEntry, field) return ensureSliceLength(unencryptedEntry, field), nil
} else { } else {
return unencryptedEntry return unencryptedEntry, nil
} }
} }
+7 -4
View File
@@ -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) 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) sshKeyIsFile, _ := back.TargetIsFile(sshKeyPath, false, 0)
if !sshKeyIsFile { 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 // 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 // perform operations based on collected user input
//// initialize libmutton directories //// 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 //// write config file
//// temporarily assign sshEntryRoot and sshIsWindows to null to pass initial device ID registration //// temporarily assign sshEntryRoot and sshIsWindows to null to pass initial device ID registration
cfg.WriteConfig(append( cfg.WriteConfig(append(
@@ -54,7 +57,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
// generate and register device ID // generate and register device ID
sshEntryRoot, sshIsWindows, err := synccycles.DeviceIDGen(oldDeviceID) sshEntryRoot, sshIsWindows, err := synccycles.DeviceIDGen(oldDeviceID)
if err != nil { 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) cfg.WriteConfig([][3]string{{"LIBMUTTON", "sshEntryRoot", sshEntryRoot}, {"LIBMUTTON", "sshIsWindows", sshIsWindows}}, nil, true)
} else { } else {
@@ -69,7 +72,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
if len(rcwPassphrase) > 0 { if len(rcwPassphrase) > 0 {
err := wrappers.GenSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", rcwPassphrase) err := wrappers.GenSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", rcwPassphrase)
if err != nil { 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 return nil
+8 -8
View File
@@ -1,6 +1,7 @@
package core package core
import ( import (
"errors"
"os" "os"
"strings" "strings"
@@ -10,12 +11,13 @@ import (
) )
// WriteEntry writes entryData to an encrypted file at targetLocation. // 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) encBytes := crypt.EncryptBytes(entryData)
err := os.WriteFile(targetLocation, encBytes, 0600) err := os.WriteFile(targetLocation, encBytes, 0600)
if err != nil { 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. // 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 // EntryAddPrecheck ensures the directory meant to contain a new
// entry exists and that the target entry location is not already used. // 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). // 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 // ensure target location does not already exist
_, isAccessible := back.TargetIsFile(targetLocation, false, 0) _, isAccessible := back.TargetIsFile(targetLocation, false, 0)
if isAccessible { if isAccessible {
back.PrintError("Target location already exists", global.ErrorTargetExists, false) return 1, errors.New("target location already exists")
return 1 // inform interactive clients that the target location already exists
} }
// ensure target containing directory exists and is a directory (not a file) // ensure target containing directory exists and is a directory (not a file)
containingDir := targetLocation[:strings.LastIndex(targetLocation, global.PathSeparator)] containingDir := targetLocation[:strings.LastIndex(targetLocation, global.PathSeparator)]
isFile, isAccessible := back.TargetIsFile(containingDir, false, 1) isFile, isAccessible := back.TargetIsFile(containingDir, false, 1)
if isFile || !isAccessible { if isFile || !isAccessible {
back.PrintError("\""+containingDir+"\" is not a valid containing directory", back.ErrorTargetWrongType, false) return 2, errors.New("\"" + containingDir + "\" is not a valid containing directory")
return 2 // inform interactive clients that the containing directory is invalid
} }
return 0 return 0, nil
} }
// EntryIsNotEmpty iterates through entryData and returns true if any line is not empty. // EntryIsNotEmpty iterates through entryData and returns true if any line is not empty.
+6 -5
View File
@@ -1,6 +1,7 @@
package crypt package crypt
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -25,26 +26,26 @@ func RCWDArgument() {
} }
// DecryptFileToSlice decrypts an RCW wrapped file and returns the contents as a slice of (trimmed) strings. // 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 // read encrypted file
encBytes, err := os.ReadFile(targetLocation) encBytes, err := os.ReadFile(targetLocation)
if err != nil { 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 // decrypt data using RCW daemon
passphrase := launchRCWDProcess() passphrase := launchRCWDProcess()
if passphrase == nil { if passphrase == nil {
// if daemon is already running, use it to decrypt the data // 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 // if the daemon is not already running, use wrappers.Decrypt
// directly to avoid waiting for socket file creation // directly to avoid waiting for socket file creation
decBytes, err := wrappers.Decrypt(encBytes, passphrase) decBytes, err := wrappers.Decrypt(encBytes, passphrase)
if err != nil { 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. // EncryptBytes encrypts a byte slice using RCW and returns the encrypted data.
+10 -12
View File
@@ -1,36 +1,34 @@
package global package global
import ( import (
"errors"
"io/fs" "io/fs"
"os" "os"
"github.com/rwinkhart/go-boilerplate/back"
) )
// GetOldDeviceID returns the current device ID or // GetOldDeviceID returns the current device ID or
// FSMisc if there is no device ID (e.g. first run). // FSMisc if there is no device ID (e.g. first run).
func GetCurrentDeviceID() string { func GetCurrentDeviceID() (string, error) {
deviceIDList := GenDeviceIDList(false) // errorOnFail is false so that nil is received when the devices directory does not exist deviceIDList, err := GenDeviceIDList()
if err != nil {
return "", errors.New("unable to generate device ID list: " + err.Error())
}
var deviceID string var deviceID string
if len(deviceIDList) > 0 { if len(deviceIDList) > 0 {
deviceID = (deviceIDList)[0].Name() deviceID = (deviceIDList)[0].Name()
} else { } else {
deviceID = FSMisc // indicates to server that no device ID is being replaced 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. // 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) // 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 // create a slice of all registered devices
deviceIDList, err := os.ReadDir(ConfigDir + PathSeparator + "devices") deviceIDList, err := os.ReadDir(ConfigDir + PathSeparator + "devices")
if err != nil { if err != nil {
if errorOnFail { return nil, errors.New("unable to read the devices directory: " + err.Error())
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 deviceIDList return deviceIDList, nil
} }
+10 -6
View File
@@ -1,6 +1,7 @@
package global package global
import ( import (
"errors"
"os" "os"
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
@@ -8,15 +9,18 @@ import (
// DirInit creates the libmutton directories. // DirInit creates the libmutton directories.
// Returns: oldDeviceID (from before the directory reset; will be FSMisc if there is no pre-existing ID). // 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 // create EntryRoot
err := os.MkdirAll(EntryRoot, 0700) err := os.MkdirAll(EntryRoot, 0700)
if err != nil { 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 // 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) // remove existing config directory (if it exists and not in append mode)
if !preserveOldConfigDir { if !preserveOldConfigDir {
@@ -24,7 +28,7 @@ func DirInit(preserveOldConfigDir bool) string {
if isAccessible { if isAccessible {
err = os.RemoveAll(ConfigDir) err = os.RemoveAll(ConfigDir)
if err != nil { 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 // create config directory w/devices subdirectory
err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700) err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700)
if err != nil { 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
} }
+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. // 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 // create a session
sshSession, err := sshClient.NewSession() sshSession, err := sshClient.NewSession()
if err != nil { 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 // provide stdin data for session
@@ -113,40 +113,46 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string {
var output []byte var output []byte
output, err = sshSession.CombinedOutput(cmd) output, err = sshSession.CombinedOutput(cmd)
if err != nil { 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 // convert the output to a string and remove leading/trailing whitespace
outputString := string(output) outputString := string(output)
outputString = strings.TrimSpace(outputString) 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. // 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 // 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 len(deviceIDList) == 0 {
if manualSync { 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 { } else {
back.Exit(0) // exit silently if the sync job was called automatically, as the user may just be in offline mode 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 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 // split output into slice based on occurrences of FSSpace
outputSlice := strings.Split(output, global.FSSpace) outputSlice := strings.Split(output, global.FSSpace)
// parse output/re-form lists // parse output/re-form lists
if len(outputSlice) != 5 { // ensure information from server is complete 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) serverTime, err := strconv.ParseInt(outputSlice[0], 10, 64)
if err != nil { 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:] entries := strings.Split(outputSlice[1], global.FSMisc)[1:]
modsStrings := strings.Split(outputSlice[2], 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 { for _, modString := range modsStrings {
mod, err = strconv.ParseInt(modString, 10, 64) mod, err = strconv.ParseInt(modString, 10, 64)
if err != nil { 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) mods = append(mods, mod)
} }
@@ -170,13 +176,16 @@ func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string
entryModMap[entry] = mods[i] 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. // 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 // 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 // get a list of all entry modification times
modList := synccommon.GetModTimes(entries) modList := synccommon.GetModTimes(entries)
@@ -188,7 +197,7 @@ func getLocalData() map[string]int64 {
} }
// return the lists // return the lists
return entryModMap return entryModMap, nil
} }
// targetLocationFormatSFTP formats the target location to match the remote server's entry directory and path separator. // 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. // 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 // create an SFTP client from sshClient
sftpClient, err := sftp.NewClient(sshClient) sftpClient, err := sftp.NewClient(sshClient)
if err != nil { 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) { defer func(sftpClient *sftp.Client) {
err = sftpClient.Close() _ = sftpClient.Close()
if err != nil {
back.PrintError("Sync failed - Unable to close SFTP client: "+err.Error(), global.ErrorServerConnection, true)
}
}(sftpClient) }(sftpClient)
// iterate over the download list // iterate over the download list
@@ -228,7 +234,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
var fileInfo os.FileInfo var fileInfo os.FileInfo
fileInfo, err = sftpClient.Stat(remoteEntryFullPath) fileInfo, err = sftpClient.Stat(remoteEntryFullPath)
if err != nil { 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() modTime := fileInfo.ModTime()
@@ -236,7 +242,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
var remoteFile *sftp.File var remoteFile *sftp.File
remoteFile, err = sftpClient.Open(remoteEntryFullPath) remoteFile, err = sftpClient.Open(remoteEntryFullPath)
if err != nil { 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 // store path to local entry
@@ -246,13 +252,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
var localFile *os.File var localFile *os.File
localFile, err = os.OpenFile(localEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) localFile, err = os.OpenFile(localEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil { 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 // download the file
_, err = remoteFile.WriteTo(localFile) _, err = remoteFile.WriteTo(localFile)
if err != nil { 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 // close the files
@@ -281,7 +287,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
var fileInfo os.FileInfo var fileInfo os.FileInfo
fileInfo, err = os.Stat(localEntryFullPath) fileInfo, err = os.Stat(localEntryFullPath)
if err != nil { 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() modTime := fileInfo.ModTime()
@@ -289,7 +295,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
var localFile *os.File var localFile *os.File
localFile, err = os.Open(localEntryFullPath) localFile, err = os.Open(localEntryFullPath)
if err != nil { 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 // store path to remote entry
@@ -299,13 +305,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
var remoteFile *sftp.File var remoteFile *sftp.File
remoteFile, err = sftpClient.OpenFile(remoteEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY) remoteFile, err = sftpClient.OpenFile(remoteEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY)
if err != nil { 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 // upload the file
_, err = localFile.WriteTo(remoteFile) _, err = localFile.WriteTo(remoteFile)
if err != nil { 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 // close the files
@@ -315,7 +321,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
// set permissions on remote file // set permissions on remote file
err = sftpClient.Chmod(remoteEntryFullPath, 0600) err = sftpClient.Chmod(remoteEntryFullPath, 0600)
if err != nil { 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) // 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 { if filesTransferred {
fmt.Println() // add a gap between upload and sync complete messages 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. // 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). // 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 var filesDeleted bool
for _, deletion := range deletions { 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) 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)") fmt.Println(synccommon.AnsiDelete+deletion+back.AnsiReset, "has been sheared, removing locally (if it exists)")
err := os.RemoveAll(global.TargetLocationFormat(deletion)) err := os.RemoveAll(global.TargetLocationFormat(deletion))
if err != nil { 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 { if filesDeleted {
fmt.Println() // add a gap between deletion and other messages 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). // 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 { for _, folder := range folders {
// store the full local path of the folder // store the full local path of the folder
folderFullPath := global.TargetLocationFormat(folder) folderFullPath := global.TargetLocationFormat(folder)
@@ -405,35 +413,33 @@ func folderSync(folders []string) {
if !isFile && !isAccessible { if !isFile && !isAccessible {
err := os.MkdirAll(folderFullPath, 0700) err := os.MkdirAll(folderFullPath, 0700)
if err != nil { 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 { } 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. // RunJob runs the SSH sync job.
// Setting manualSync to true will throw errors if sync is not configured (online mode is assumed). // 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. // 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 // get SSH client to re-use throughout the sync process
sshClient, sshEntryRoot, sshIsWindows, err := GetSSHClient(manualSync) sshClient, sshEntryRoot, sshIsWindows, err := GetSSHClient(manualSync)
if err != nil { if err != nil {
back.PrintError("sync failed - unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true) return [3][]string{nil, nil, nil}, errors.New("unable to connect to SSH client: " + err.Error())
}
if sshClient == nil { // indicate SSH dialing failure for interactive clients
return [3][]string{nil, nil, nil}
} }
defer func(sshClient *ssh.Client) { defer func(sshClient *ssh.Client) {
err := sshClient.Close() _ = sshClient.Close()
if err != nil {
back.PrintError("sync failed - unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
}
}(sshClient) }(sshClient)
// fetch remote lists // 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 // sync deletions
deletionSync(deletions) deletionSync(deletions)
@@ -442,7 +448,10 @@ func RunJob(manualSync, returnLists bool) [3][]string {
folderSync(remoteFolders) folderSync(remoteFolders)
// fetch local lists // 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 // prior to syncing lists, ensure the client and server clocks are synced within 45 seconds
var timeSynced = true var timeSynced = true
@@ -457,9 +466,9 @@ func RunJob(manualSync, returnLists bool) [3][]string {
if returnLists { if returnLists {
lists = syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap) lists = syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap)
lists[0] = deletions lists[0] = deletions
return lists return lists, nil
} }
syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, false, localEntryModMap, remoteEntryModMap) syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, false, localEntryModMap, remoteEntryModMap)
back.Exit(0) // exit program if running non-interactively back.Exit(0) // exit program if running non-interactively
return lists // dummy return for when not returning lists return lists, nil // dummy return for when not returning lists
} }
+25 -12
View File
@@ -1,6 +1,7 @@
package syncclient package syncclient
import ( import (
"errors"
"strings" "strings"
"github.com/rwinkhart/go-boilerplate/back" "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. // 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). // 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) { func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) error {
deviceID, isDir := synccommon.ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client 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) 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 // create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
sshClient, _, _, err := GetSSHClient(false) sshClient, _, _, err := GetSSHClient(false)
if err != nil { 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) // 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 // close the SSH client
err = sshClient.Close() err = sshClient.Close()
if err != nil { 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 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. // 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). // 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 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) 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 // create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
sshClient, _, _, err := GetSSHClient(false) sshClient, _, _, err := GetSSHClient(false)
if err != nil { 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 // 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 // close the SSH client
err = sshClient.Close() err = sshClient.Close()
if err != nil { 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) 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. // 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). // 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 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) 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 // create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
sshClient, _, _, err := GetSSHClient(false) sshClient, _, _, err := GetSSHClient(false)
if err != nil { 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 // call the server to create the folder remotely
@@ -86,9 +98,10 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline boo
// close the SSH client // close the SSH client
err = sshClient.Close() err = sshClient.Close()
if err != nil { 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) back.Exit(0)
return nil
} }
+19 -11
View File
@@ -1,6 +1,7 @@
package synccommon package synccommon
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@@ -34,14 +35,17 @@ func GetModTimes(entryList []string) []int64 {
// isDir (only on client; for use in ShearRemoteFromClient). // 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). // 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. // 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 // determine if running on a server
var onServer bool var onServer bool
if clientDeviceID != "" { if clientDeviceID != "" {
onServer = true 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) // add the sheared target (incomplete, vanity) to the deletions list (if running on a server)
if onServer { 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 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) isFile, _ = back.TargetIsFile(targetLocationComplete, true, 0)
} }
err := os.RemoveAll(targetLocationComplete) err = os.RemoveAll(targetLocationComplete)
if err != nil { 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) 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 // do not exit program, as this function is used as part of ShearRemoteFromClient
} }
// RenameLocal renames oldLocationIncomplete to newLocationIncomplete on the local system. // RenameLocal renames oldLocationIncomplete to newLocationIncomplete on the local system.
// This function should only be used directly by the server binary. // 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 // get full paths for both locations
oldLocation := global.TargetLocationFormat(oldLocationIncomplete) oldLocation := global.TargetLocationFormat(oldLocationIncomplete)
newLocation := global.TargetLocationFormat(newLocationIncomplete) newLocation := global.TargetLocationFormat(newLocationIncomplete)
@@ -91,21 +95,23 @@ func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldL
// ensure newLocation does not exist // ensure newLocation does not exist
_, isAccessible := back.TargetIsFile(newLocation, false, 0) _, isAccessible := back.TargetIsFile(newLocation, false, 0)
if isAccessible { if isAccessible {
back.PrintError("\""+newLocation+"\" already exists", global.ErrorTargetExists, true) return errors.New("target already exists: " + newLocation)
} }
// rename oldLocation to newLocation // rename oldLocation to newLocation
err := os.Rename(oldLocation, newLocation) err := os.Rename(oldLocation, newLocation)
if err != nil { 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 // do not exit program, as this function is used as part of RenameRemoteFromClient
} }
// AddFolderLocal creates a new entry-containing directory on the local system. // AddFolderLocal creates a new entry-containing directory on the local system.
// This function should only be used directly by the server binary. // 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 // get the full targetLocation path and create the target
targetLocationComplete := global.TargetLocationFormat(targetLocationIncomplete) targetLocationComplete := global.TargetLocationFormat(targetLocationIncomplete)
err := os.Mkdir(targetLocationComplete, 0700) err := os.Mkdir(targetLocationComplete, 0700)
@@ -113,9 +119,11 @@ func AddFolderLocal(targetLocationIncomplete string) {
if os.IsExist(err) { if os.IsExist(err) {
fmt.Println(AnsiUpload + "Directory already exists - libmutton will still ensure it exists on the server") fmt.Println(AnsiUpload + "Directory already exists - libmutton will still ensure it exists on the server")
} else { } 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 // do not exit program, as this function is used as part of AddFolderRemoteFromClient
} }
+9 -7
View File
@@ -3,31 +3,31 @@
package synccommon package synccommon
import ( import (
"errors"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/global" "github.com/rwinkhart/libmutton/global"
) )
// WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists). // 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). // 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 // define file/directory containing slices so that they may be accessed by the anonymous WalkDir function
var fileList []string var fileList []string
var dirList []string var dirList []string
// walk entry directory // walk entry directory
_ = filepath.WalkDir(global.EntryRoot, err := filepath.WalkDir(global.EntryRoot,
func(fullPath string, entry fs.DirEntry, err error) error { func(fullPath string, entry fs.DirEntry, err error) error {
// check for errors encountered while walking directory // check for errors encountered while walking directory
if err != nil { if err != nil {
if os.IsNotExist(err) { 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 { } 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 nil
}) })
if err != nil {
return fileList, dirList return nil, nil, err
}
return fileList, dirList, nil
} }
+9 -7
View File
@@ -3,32 +3,32 @@
package synccommon package synccommon
import ( import (
"errors"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/global" "github.com/rwinkhart/libmutton/global"
) )
// WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists). // 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). // 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 // define file/directory containing slices so that they may be accessed by the anonymous WalkDir function
var fileList []string var fileList []string
var dirList []string var dirList []string
// walk entry directory // walk entry directory
_ = filepath.WalkDir(global.EntryRoot, err := filepath.WalkDir(global.EntryRoot,
func(fullPath string, entry fs.DirEntry, err error) error { func(fullPath string, entry fs.DirEntry, err error) error {
// check for errors encountered while walking directory // check for errors encountered while walking directory
if err != nil { if err != nil {
if os.IsNotExist(err) { 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 { } 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 nil
}) })
if err != nil {
return fileList, dirList return nil, nil, err
}
return fileList, dirList, nil
} }
+9 -5
View File
@@ -25,14 +25,14 @@ func DeviceIDGen(oldDeviceID string) (string, string, error) {
// create new device ID file (locally) // 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) fileToClose, err := os.OpenFile(global.ConfigDir+global.PathSeparator+"devices"+global.PathSeparator+newDeviceID, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil { 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 _ = fileToClose.Close() // error ignored; if the file could be created, it can probably be closed
// remove old device ID file (locally; may not exist) // remove old device ID file (locally; may not exist)
err = os.RemoveAll(global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + oldDeviceID) err = os.RemoveAll(global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + oldDeviceID)
if err != nil { 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 // 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 // manualSync is true so the user is alerted if device ID registration fails
sshClient, _, _, err := syncclient.GetSSHClient(true) sshClient, _, _, err := syncclient.GetSSHClient(true)
if err != nil { 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() err = sshClient.Close()
if err != nil { 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 return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1], nil
+8 -4
View File
@@ -1,12 +1,12 @@
package syncserver package syncserver
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/global" "github.com/rwinkhart/libmutton/global"
"github.com/rwinkhart/libmutton/synccommon" "github.com/rwinkhart/libmutton/synccommon"
) )
@@ -14,12 +14,15 @@ import (
// GetRemoteDataFromServer prints to stdout the remote entries, mod times, folders, and deletions. // GetRemoteDataFromServer prints to stdout the remote entries, mod times, folders, and deletions.
// Lists in output are separated by FSSpace. // Lists in output are separated by FSSpace.
// Output is meant to be captured over SSH for interpretation by the client. // Output is meant to be captured over SSH for interpretation by the client.
func GetRemoteDataFromServer(clientDeviceID string) { func GetRemoteDataFromServer(clientDeviceID string) error {
entryList, dirList := synccommon.WalkEntryDir() entryList, dirList, err := synccommon.WalkEntryDir()
if err != nil {
return errors.New("unable to walk the entry directory: " + err.Error())
}
modList := synccommon.GetModTimes(entryList) modList := synccommon.GetModTimes(entryList)
deletionsList, err := os.ReadDir(global.ConfigDir + global.PathSeparator + "deletions") deletionsList, err := os.ReadDir(global.ConfigDir + global.PathSeparator + "deletions")
if err != nil { 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 // 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 _ = 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
} }