mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-28 04:46:42 -04:00
Return errors, rather than printing them
This commit is contained in:
+18
-11
@@ -1,6 +1,7 @@
|
||||
package cfg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
@@ -10,12 +11,12 @@ import (
|
||||
|
||||
// loadConfig loads the libmutton.ini file and returns the configuration.
|
||||
// It is a utility function for ParseConfig and WriteConfig; do not call directly.
|
||||
func loadConfig() *ini.File {
|
||||
func loadConfig() (*ini.File, error) {
|
||||
cfg, err := ini.Load(global.ConfigPath)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to load libmutton.ini: "+err.Error(), back.ErrorRead, true)
|
||||
return nil, errors.New("unable to load libmutton.ini: " + err.Error())
|
||||
}
|
||||
return cfg
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// ParseConfig reads the libmutton.ini file and returns a slice of values for the specified keys.
|
||||
@@ -25,7 +26,10 @@ func loadConfig() *ini.File {
|
||||
// error (nil if no error occurred, otherwise an error using the generated or provided message).
|
||||
func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]string, error) {
|
||||
var err error
|
||||
cfg := loadConfig()
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config []string
|
||||
|
||||
@@ -36,14 +40,12 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin
|
||||
if value == "" {
|
||||
switch missingValueError {
|
||||
case "":
|
||||
err = fmt.Errorf("failed to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
|
||||
err = fmt.Errorf("unable to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
|
||||
case "0":
|
||||
back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
|
||||
default:
|
||||
err = fmt.Errorf("%s", missingValueError)
|
||||
}
|
||||
back.PrintError(err.Error(), back.ErrorRead, false)
|
||||
// if interactive (soft exit), return nil and the error to be handled by the caller
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -57,12 +59,16 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin
|
||||
// Requires: valuesToWrite (a slice of length 3 arrays each containing a section, a key name, and a value),
|
||||
// prune (a slice similar to valuesToWrite to allow removing the specified keys from an existing config),
|
||||
// append (set to true to append to the existing libmutton.ini file, false to overwrite it).
|
||||
func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool) {
|
||||
func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool) error {
|
||||
var cfg *ini.File
|
||||
var err error
|
||||
|
||||
if append {
|
||||
// load existing ini file
|
||||
cfg = loadConfig()
|
||||
cfg, err = loadConfig()
|
||||
if err != nil {
|
||||
return errors.New("unable to load existing libmutton.ini: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
// create empty ini container
|
||||
cfg = ini.Empty()
|
||||
@@ -93,8 +99,9 @@ func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool
|
||||
|
||||
// save to libmutton.ini
|
||||
setUmask(0077) // only give permissions to owner
|
||||
err := cfg.SaveTo(global.ConfigPath)
|
||||
err = cfg.SaveTo(global.ConfigPath)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to save libmutton.ini: "+err.Error(), back.ErrorWrite, true)
|
||||
return errors.New("unable to save libmutton.ini: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,30 +3,31 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// clipClearProcess clears the clipboard after 30 seconds if the clipboard contents have not changed.
|
||||
// assignedContents can be omitted to clear the clipboard immediately and unconditionally.
|
||||
func clipClearProcess(assignedContents string) {
|
||||
func clipClearProcess(assignedContents string) error {
|
||||
cmdPaste, cmdClear := getClipCommands()
|
||||
|
||||
clearClipboard := func() {
|
||||
clearClipboard := func() error {
|
||||
err := cmdClear.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to clear clipboard", global.ErrorClipboard, true)
|
||||
return errors.New("unable to clear clipboard")
|
||||
}
|
||||
back.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// if assignedContents is empty, clear the clipboard immediately and unconditionally
|
||||
if assignedContents == "" {
|
||||
clearClipboard()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// wait 30 seconds before checking clipboard contents
|
||||
@@ -34,10 +35,14 @@ func clipClearProcess(assignedContents string) {
|
||||
|
||||
newContents, err := cmdPaste.Output()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to read clipboard contents", global.ErrorClipboard, true)
|
||||
return errors.New("unable to read clipboard contents")
|
||||
}
|
||||
|
||||
if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||
clearClipboard()
|
||||
err := clearClipboard()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+17
-8
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -13,10 +14,13 @@ import (
|
||||
)
|
||||
|
||||
// CopyArgument copies a field from an entry to the clipboard.
|
||||
func CopyArgument(targetLocation string, field int) {
|
||||
func CopyArgument(targetLocation string, field int) error {
|
||||
if isFile, _ := back.TargetIsFile(targetLocation, true, 2); isFile {
|
||||
|
||||
decryptedEntry := crypt.DecryptFileToSlice(targetLocation)
|
||||
decryptedEntry, err := crypt.DecryptFileToSlice(targetLocation)
|
||||
if err != nil {
|
||||
return errors.New("unable to decrypt entry: " + err.Error())
|
||||
}
|
||||
var copySubject string // will store data to be copied
|
||||
|
||||
// ensure field exists in entry
|
||||
@@ -24,7 +28,7 @@ func CopyArgument(targetLocation string, field int) {
|
||||
|
||||
// ensure field is not empty
|
||||
if decryptedEntry[field] == "" {
|
||||
back.PrintError("Field is empty", back.ErrorTargetNotFound, true)
|
||||
return errors.New("field is empty")
|
||||
}
|
||||
|
||||
if field != 2 {
|
||||
@@ -44,18 +48,23 @@ func CopyArgument(targetLocation string, field int) {
|
||||
|
||||
for { // keep token copied to clipboard, refresh on 30-second intervals
|
||||
currentTime := time.Now()
|
||||
copyString(true, GenTOTP(secret, currentTime, forSteam))
|
||||
token, err := GenTOTP(secret, currentTime, forSteam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copyString(true, token)
|
||||
// sleep until next 30-second interval
|
||||
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
back.PrintError("Field does not exist in entry", back.ErrorTargetNotFound, true)
|
||||
return errors.New("field does not exist in entry")
|
||||
}
|
||||
|
||||
// copy field to clipboard, launch clipboard clearing process
|
||||
copyString(false, copySubject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess.
|
||||
@@ -68,7 +77,7 @@ func ClipClearArgument() {
|
||||
}
|
||||
|
||||
// GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP).
|
||||
func GenTOTP(secret string, time time.Time, forSteam bool) string {
|
||||
func GenTOTP(secret string, time time.Time, forSteam bool) (string, error) {
|
||||
var totpToken string
|
||||
var err error
|
||||
|
||||
@@ -79,8 +88,8 @@ func GenTOTP(secret string, time time.Time, forSteam bool) string {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
back.PrintError("Error generating TOTP code: "+err.Error(), back.ErrorOther, true)
|
||||
return "", errors.New("error generating TOTP code: " + err.Error())
|
||||
}
|
||||
|
||||
return totpToken
|
||||
return totpToken, nil
|
||||
}
|
||||
|
||||
+4
-4
@@ -3,24 +3,24 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
func copyString(continuous bool, copySubject string) error {
|
||||
cmd := exec.Command("pbcopy")
|
||||
back.WriteToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true)
|
||||
return errors.New("unable to copy to clipboard: " + err.Error())
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
LaunchClipClearProcess(copySubject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
// TODO Investigate background clipboard clearing and on-app-close clipboard clearing for Android
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
clipboard.Write(clipboard.FmtText, []byte(copySubject))
|
||||
|
||||
+4
-4
@@ -3,24 +3,24 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
func copyString(continuous bool, copySubject string) error {
|
||||
cmd := exec.Command("termux-clipboard-set")
|
||||
back.WriteToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true)
|
||||
return errors.New("unable to copy to clipboard: " + err.Error())
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
LaunchClipClearProcess(copySubject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||
|
||||
+6
-6
@@ -3,36 +3,36 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
func copyString(continuous bool, copySubject string) error {
|
||||
// determine whether to use wl-copy (Wayland) or xclip (X11)
|
||||
var envSet, isWayland bool // track whether environment variables are set
|
||||
var cmdCopy *exec.Cmd
|
||||
// determine whether to use wl-copy (Wayland) or xclip (X11)
|
||||
if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet {
|
||||
cmdCopy = exec.Command("wl-copy", "-t", "text/plain")
|
||||
isWayland = true
|
||||
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
|
||||
cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain")
|
||||
} else {
|
||||
back.PrintError("Clipboard platform could not be determined", global.ErrorClipboard, true)
|
||||
return errors.New("clipboard platform could not be determined")
|
||||
}
|
||||
|
||||
back.WriteToStdin(cmdCopy, copySubject)
|
||||
err := cmdCopy.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true)
|
||||
return errors.New("unable to copy to clipboard: " + err.Error())
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
LaunchClipClearProcess(copySubject, isWayland)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||
|
||||
+4
-6
@@ -3,25 +3,23 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
func copyString(continuous bool, copySubject string) error {
|
||||
cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true)
|
||||
return errors.New("unable to copy to clipboard: " + err.Error())
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
LaunchClipClearProcess(copySubject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||
|
||||
+9
-4
@@ -1,23 +1,28 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/crypt"
|
||||
)
|
||||
|
||||
// GetOldEntryData decrypts and returns old entry data (with all required lines present).
|
||||
func GetOldEntryData(targetLocation string, field int) []string {
|
||||
func GetOldEntryData(targetLocation string, field int) ([]string, error) {
|
||||
// ensure targetLocation exists
|
||||
back.TargetIsFile(targetLocation, true, 2)
|
||||
|
||||
// read old entry data
|
||||
unencryptedEntry := crypt.DecryptFileToSlice(targetLocation)
|
||||
unencryptedEntry, err := crypt.DecryptFileToSlice(targetLocation)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to decrypt entry: " + err.Error())
|
||||
}
|
||||
|
||||
// return the old entry data with all required lines present
|
||||
if field > 0 {
|
||||
return ensureSliceLength(unencryptedEntry, field)
|
||||
return ensureSliceLength(unencryptedEntry, field), nil
|
||||
} else {
|
||||
return unencryptedEntry
|
||||
return unencryptedEntry, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -23,7 +23,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
|
||||
sshKeyPath := cmp.Or(back.ExpandPathWithHome(inputCB(back.AnsiBold+"Note:"+back.AnsiReset+" Only key-based authentication is supported (keys may optionally be passphrase-protected).\n The remote server must already be in your ~"+global.PathSeparator+".ssh"+global.PathSeparator+"known_hosts file.\n\nSSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey)
|
||||
sshKeyIsFile, _ := back.TargetIsFile(sshKeyPath, false, 0)
|
||||
if !sshKeyIsFile {
|
||||
return errors.New("ssh identity file not found: " + sshKeyPath)
|
||||
return errors.New("SSH identity file not found: " + sshKeyPath)
|
||||
}
|
||||
|
||||
// get other ssh info from user
|
||||
@@ -38,7 +38,10 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
|
||||
|
||||
// perform operations based on collected user input
|
||||
//// initialize libmutton directories
|
||||
oldDeviceID := global.DirInit(preserveOldConfigDir)
|
||||
oldDeviceID, err := global.DirInit(preserveOldConfigDir)
|
||||
if err != nil {
|
||||
return errors.New("unable to initialize libmutton directories: " + err.Error())
|
||||
}
|
||||
//// write config file
|
||||
//// temporarily assign sshEntryRoot and sshIsWindows to null to pass initial device ID registration
|
||||
cfg.WriteConfig(append(
|
||||
@@ -54,7 +57,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
|
||||
// generate and register device ID
|
||||
sshEntryRoot, sshIsWindows, err := synccycles.DeviceIDGen(oldDeviceID)
|
||||
if err != nil {
|
||||
return errors.New("failed to generate device ID: " + err.Error())
|
||||
return errors.New("unable to generate device ID: " + err.Error())
|
||||
}
|
||||
cfg.WriteConfig([][3]string{{"LIBMUTTON", "sshEntryRoot", sshEntryRoot}, {"LIBMUTTON", "sshIsWindows", sshIsWindows}}, nil, true)
|
||||
} else {
|
||||
@@ -69,7 +72,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
|
||||
if len(rcwPassphrase) > 0 {
|
||||
err := wrappers.GenSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", rcwPassphrase)
|
||||
if err != nil {
|
||||
return errors.New("failed to generate sanity check file: " + err.Error())
|
||||
return errors.New("unable to generate sanity check file: " + err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -10,12 +11,13 @@ import (
|
||||
)
|
||||
|
||||
// WriteEntry writes entryData to an encrypted file at targetLocation.
|
||||
func WriteEntry(targetLocation string, entryData []byte) {
|
||||
func WriteEntry(targetLocation string, entryData []byte) error {
|
||||
encBytes := crypt.EncryptBytes(entryData)
|
||||
err := os.WriteFile(targetLocation, encBytes, 0600)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to write to file: "+err.Error(), back.ErrorWrite, true)
|
||||
return errors.New("unable to write to file: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClampTrailingWhitespace strips trailing newlines, carriage returns, and tabs from each line in a note.
|
||||
@@ -51,21 +53,19 @@ func ClampTrailingWhitespace(note []string) {
|
||||
// EntryAddPrecheck ensures the directory meant to contain a new
|
||||
// entry exists and that the target entry location is not already used.
|
||||
// Returns: statusCode (0 = success, 1 = target location already exists, 2 = containing directory is invalid).
|
||||
func EntryAddPrecheck(targetLocation string) uint8 {
|
||||
func EntryAddPrecheck(targetLocation string) (uint8, error) {
|
||||
// ensure target location does not already exist
|
||||
_, isAccessible := back.TargetIsFile(targetLocation, false, 0)
|
||||
if isAccessible {
|
||||
back.PrintError("Target location already exists", global.ErrorTargetExists, false)
|
||||
return 1 // inform interactive clients that the target location already exists
|
||||
return 1, errors.New("target location already exists")
|
||||
}
|
||||
// ensure target containing directory exists and is a directory (not a file)
|
||||
containingDir := targetLocation[:strings.LastIndex(targetLocation, global.PathSeparator)]
|
||||
isFile, isAccessible := back.TargetIsFile(containingDir, false, 1)
|
||||
if isFile || !isAccessible {
|
||||
back.PrintError("\""+containingDir+"\" is not a valid containing directory", back.ErrorTargetWrongType, false)
|
||||
return 2 // inform interactive clients that the containing directory is invalid
|
||||
return 2, errors.New("\"" + containingDir + "\" is not a valid containing directory")
|
||||
}
|
||||
return 0
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// EntryIsNotEmpty iterates through entryData and returns true if any line is not empty.
|
||||
|
||||
+6
-5
@@ -1,6 +1,7 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -25,26 +26,26 @@ func RCWDArgument() {
|
||||
}
|
||||
|
||||
// DecryptFileToSlice decrypts an RCW wrapped file and returns the contents as a slice of (trimmed) strings.
|
||||
func DecryptFileToSlice(targetLocation string) []string {
|
||||
func DecryptFileToSlice(targetLocation string) ([]string, error) {
|
||||
// read encrypted file
|
||||
encBytes, err := os.ReadFile(targetLocation)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to open \""+targetLocation+"\" for decryption - "+err.Error(), back.ErrorRead, true)
|
||||
return nil, errors.New("unable to open \"" + targetLocation + "\" for decryption: " + err.Error())
|
||||
}
|
||||
|
||||
// decrypt data using RCW daemon
|
||||
passphrase := launchRCWDProcess()
|
||||
if passphrase == nil {
|
||||
// if daemon is already running, use it to decrypt the data
|
||||
return strings.Split(string(daemon.GetDec(encBytes)), "\n")
|
||||
return strings.Split(string(daemon.GetDec(encBytes)), "\n"), nil
|
||||
}
|
||||
// if the daemon is not already running, use wrappers.Decrypt
|
||||
// directly to avoid waiting for socket file creation
|
||||
decBytes, err := wrappers.Decrypt(encBytes, passphrase)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), global.ErrorDecryption, true)
|
||||
return nil, errors.New("unable to decrypt \"" + targetLocation + "\": " + err.Error())
|
||||
}
|
||||
return strings.Split(string(decBytes), "\n")
|
||||
return strings.Split(string(decBytes), "\n"), nil
|
||||
}
|
||||
|
||||
// EncryptBytes encrypts a byte slice using RCW and returns the encrypted data.
|
||||
|
||||
+10
-12
@@ -1,36 +1,34 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
)
|
||||
|
||||
// GetOldDeviceID returns the current device ID or
|
||||
// FSMisc if there is no device ID (e.g. first run).
|
||||
func GetCurrentDeviceID() string {
|
||||
deviceIDList := GenDeviceIDList(false) // errorOnFail is false so that nil is received when the devices directory does not exist
|
||||
func GetCurrentDeviceID() (string, error) {
|
||||
deviceIDList, err := GenDeviceIDList()
|
||||
if err != nil {
|
||||
return "", errors.New("unable to generate device ID list: " + err.Error())
|
||||
}
|
||||
var deviceID string
|
||||
if len(deviceIDList) > 0 {
|
||||
deviceID = (deviceIDList)[0].Name()
|
||||
} else {
|
||||
deviceID = FSMisc // indicates to server that no device ID is being replaced
|
||||
}
|
||||
return deviceID
|
||||
return deviceID, nil
|
||||
}
|
||||
|
||||
// GenDeviceIDList returns a slice of all registered device IDs.
|
||||
// Requires: errorOnFail (set to true to throw an error if the devices directory cannot be read/does not exist)
|
||||
func GenDeviceIDList(errorOnFail bool) []fs.DirEntry {
|
||||
func GenDeviceIDList() ([]fs.DirEntry, error) {
|
||||
// create a slice of all registered devices
|
||||
deviceIDList, err := os.ReadDir(ConfigDir + PathSeparator + "devices")
|
||||
if err != nil {
|
||||
if errorOnFail {
|
||||
back.PrintError("Failed to read the devices directory: "+err.Error(), back.ErrorRead, true)
|
||||
} else {
|
||||
return nil // a nil return value indicates that the devices directory could not be read/does not exist
|
||||
}
|
||||
return nil, errors.New("unable to read the devices directory: " + err.Error())
|
||||
}
|
||||
return deviceIDList
|
||||
return deviceIDList, nil
|
||||
}
|
||||
|
||||
+10
-6
@@ -1,6 +1,7 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
@@ -8,15 +9,18 @@ import (
|
||||
|
||||
// DirInit creates the libmutton directories.
|
||||
// Returns: oldDeviceID (from before the directory reset; will be FSMisc if there is no pre-existing ID).
|
||||
func DirInit(preserveOldConfigDir bool) string {
|
||||
func DirInit(preserveOldConfigDir bool) (string, error) {
|
||||
// create EntryRoot
|
||||
err := os.MkdirAll(EntryRoot, 0700)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), back.ErrorWrite, true)
|
||||
return "", errors.New("unable to create \"" + EntryRoot + "\": " + err.Error())
|
||||
}
|
||||
|
||||
// get old device ID before its potential removal
|
||||
oldDeviceID := GetCurrentDeviceID()
|
||||
oldDeviceID, err := GetCurrentDeviceID()
|
||||
if err != nil {
|
||||
return "", errors.New("unable to get current device ID: " + err.Error())
|
||||
}
|
||||
|
||||
// remove existing config directory (if it exists and not in append mode)
|
||||
if !preserveOldConfigDir {
|
||||
@@ -24,7 +28,7 @@ func DirInit(preserveOldConfigDir bool) string {
|
||||
if isAccessible {
|
||||
err = os.RemoveAll(ConfigDir)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to remove existing config directory: "+err.Error(), back.ErrorWrite, true)
|
||||
return "", errors.New("unable to remove existing config directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,8 +36,8 @@ func DirInit(preserveOldConfigDir bool) string {
|
||||
// create config directory w/devices subdirectory
|
||||
err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to create \""+ConfigDir+"\": "+err.Error(), back.ErrorWrite, true)
|
||||
return "", errors.New("unable to create \"" + ConfigDir + "\": " + err.Error())
|
||||
}
|
||||
|
||||
return oldDeviceID
|
||||
return oldDeviceID, nil
|
||||
}
|
||||
|
||||
+59
-50
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+19
-11
@@ -1,6 +1,7 @@
|
||||
package synccommon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -34,14 +35,17 @@ func GetModTimes(entryList []string) []int64 {
|
||||
// isDir (only on client; for use in ShearRemoteFromClient).
|
||||
// If the local system is a server, it will also add the target to the deletions list for all clients (except the requesting client).
|
||||
// This function should only be used directly by the server binary.
|
||||
func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool) {
|
||||
func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool, error) {
|
||||
// determine if running on a server
|
||||
var onServer bool
|
||||
if clientDeviceID != "" {
|
||||
onServer = true
|
||||
}
|
||||
|
||||
deviceIDList := global.GenDeviceIDList(true)
|
||||
deviceIDList, err := global.GenDeviceIDList()
|
||||
if err != nil {
|
||||
return "", false, errors.New("unable to generate device ID list: " + err.Error())
|
||||
}
|
||||
|
||||
// add the sheared target (incomplete, vanity) to the deletions list (if running on a server)
|
||||
if onServer {
|
||||
@@ -64,22 +68,22 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool)
|
||||
if !onServer { // error if target does not exist on client, needed because os.RemoveAll does not return an error if target does not exist
|
||||
isFile, _ = back.TargetIsFile(targetLocationComplete, true, 0)
|
||||
}
|
||||
err := os.RemoveAll(targetLocationComplete)
|
||||
err = os.RemoveAll(targetLocationComplete)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to remove local target: "+err.Error(), back.ErrorWrite, true)
|
||||
return "", false, errors.New("unable to remove local target: " + err.Error())
|
||||
}
|
||||
|
||||
if !onServer && len(deviceIDList) > 0 { // return the device ID if running on the client and a device ID exists (online mode)
|
||||
return (deviceIDList)[0].Name(), !isFile
|
||||
return (deviceIDList)[0].Name(), !isFile, nil
|
||||
}
|
||||
return "", true
|
||||
return "", true, nil
|
||||
|
||||
// do not exit program, as this function is used as part of ShearRemoteFromClient
|
||||
}
|
||||
|
||||
// RenameLocal renames oldLocationIncomplete to newLocationIncomplete on the local system.
|
||||
// This function should only be used directly by the server binary.
|
||||
func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldLocationExists bool) {
|
||||
func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldLocationExists bool) error {
|
||||
// get full paths for both locations
|
||||
oldLocation := global.TargetLocationFormat(oldLocationIncomplete)
|
||||
newLocation := global.TargetLocationFormat(newLocationIncomplete)
|
||||
@@ -91,21 +95,23 @@ func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldL
|
||||
// ensure newLocation does not exist
|
||||
_, isAccessible := back.TargetIsFile(newLocation, false, 0)
|
||||
if isAccessible {
|
||||
back.PrintError("\""+newLocation+"\" already exists", global.ErrorTargetExists, true)
|
||||
return errors.New("target already exists: " + newLocation)
|
||||
}
|
||||
|
||||
// rename oldLocation to newLocation
|
||||
err := os.Rename(oldLocation, newLocation)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to rename - Does the target containing directory exist?", back.ErrorTargetNotFound, true)
|
||||
return errors.New("unable to rename: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
// do not exit program, as this function is used as part of RenameRemoteFromClient
|
||||
}
|
||||
|
||||
// AddFolderLocal creates a new entry-containing directory on the local system.
|
||||
// This function should only be used directly by the server binary.
|
||||
func AddFolderLocal(targetLocationIncomplete string) {
|
||||
func AddFolderLocal(targetLocationIncomplete string) error {
|
||||
// get the full targetLocation path and create the target
|
||||
targetLocationComplete := global.TargetLocationFormat(targetLocationIncomplete)
|
||||
err := os.Mkdir(targetLocationComplete, 0700)
|
||||
@@ -113,9 +119,11 @@ func AddFolderLocal(targetLocationIncomplete string) {
|
||||
if os.IsExist(err) {
|
||||
fmt.Println(AnsiUpload + "Directory already exists - libmutton will still ensure it exists on the server")
|
||||
} else {
|
||||
back.PrintError("Failed to create directory: "+err.Error(), back.ErrorWrite, true)
|
||||
return errors.New("unable to create directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
// do not exit program, as this function is used as part of AddFolderRemoteFromClient
|
||||
}
|
||||
|
||||
@@ -3,31 +3,31 @@
|
||||
package synccommon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists).
|
||||
// Regardless of platform, all paths are stored with forward slashes (UNIX-style).
|
||||
func WalkEntryDir() ([]string, []string) {
|
||||
func WalkEntryDir() ([]string, []string, error) {
|
||||
// define file/directory containing slices so that they may be accessed by the anonymous WalkDir function
|
||||
var fileList []string
|
||||
var dirList []string
|
||||
|
||||
// walk entry directory
|
||||
_ = filepath.WalkDir(global.EntryRoot,
|
||||
err := filepath.WalkDir(global.EntryRoot,
|
||||
func(fullPath string, entry fs.DirEntry, err error) error {
|
||||
|
||||
// check for errors encountered while walking directory
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
back.PrintError("The entry directory does not exist - Initialize libmutton to create it", back.ErrorOther, true)
|
||||
return errors.New("entry directory does not exist; initialize libmutton to create it")
|
||||
} else {
|
||||
back.PrintError("An unexpected error occurred while generating the entry list: "+err.Error(), back.ErrorOther, true)
|
||||
return errors.New("an unexpected error occurred while generating the entry list: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ func WalkEntryDir() ([]string, []string) {
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return fileList, dirList
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return fileList, dirList, nil
|
||||
}
|
||||
|
||||
@@ -3,32 +3,32 @@
|
||||
package synccommon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists).
|
||||
// Regardless of platform, all paths are stored with forward slashes (UNIX-style).
|
||||
func WalkEntryDir() ([]string, []string) {
|
||||
func WalkEntryDir() ([]string, []string, error) {
|
||||
// define file/directory containing slices so that they may be accessed by the anonymous WalkDir function
|
||||
var fileList []string
|
||||
var dirList []string
|
||||
|
||||
// walk entry directory
|
||||
_ = filepath.WalkDir(global.EntryRoot,
|
||||
err := filepath.WalkDir(global.EntryRoot,
|
||||
func(fullPath string, entry fs.DirEntry, err error) error {
|
||||
|
||||
// check for errors encountered while walking directory
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
back.PrintError("The entry directory does not exist - Initialize libmutton to create it", back.ErrorOther, true)
|
||||
return errors.New("entry directory does not exist; initialize libmutton to create it")
|
||||
} else {
|
||||
back.PrintError("An unexpected error occurred while generating the entry list: "+err.Error(), back.ErrorOther, true)
|
||||
return errors.New("an unexpected error occurred while generating the entry list: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ func WalkEntryDir() ([]string, []string) {
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return fileList, dirList
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return fileList, dirList, nil
|
||||
}
|
||||
|
||||
+9
-5
@@ -25,14 +25,14 @@ func DeviceIDGen(oldDeviceID string) (string, string, error) {
|
||||
// create new device ID file (locally)
|
||||
fileToClose, err := os.OpenFile(global.ConfigDir+global.PathSeparator+"devices"+global.PathSeparator+newDeviceID, os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", "", errors.New("failed to create local device ID file: " + err.Error())
|
||||
return "", "", errors.New("unable to create local device ID file: " + err.Error())
|
||||
}
|
||||
_ = fileToClose.Close() // error ignored; if the file could be created, it can probably be closed
|
||||
|
||||
// remove old device ID file (locally; may not exist)
|
||||
err = os.RemoveAll(global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + oldDeviceID)
|
||||
if err != nil {
|
||||
return "", "", errors.New("failed to remove old device ID file (locally): " + err.Error())
|
||||
return "", "", errors.New("unable to remove old device ID file (locally): " + err.Error())
|
||||
}
|
||||
|
||||
// register new device ID with server and fetch remote EntryRoot and OS type
|
||||
@@ -40,12 +40,16 @@ func DeviceIDGen(oldDeviceID string) (string, string, error) {
|
||||
// manualSync is true so the user is alerted if device ID registration fails
|
||||
sshClient, _, _, err := syncclient.GetSSHClient(true)
|
||||
if err != nil {
|
||||
return "", "", errors.New("device ID gen failed - unable to connect to SSH client: " + err.Error())
|
||||
return "", "", errors.New("unable to connect to SSH client: " + err.Error())
|
||||
}
|
||||
sshEntryRootSSHIsWindows := strings.Split(syncclient.GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), global.FSSpace)
|
||||
output, err := syncclient.GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID)
|
||||
if err != nil {
|
||||
return "", "", errors.New("unable to register device ID with server: " + err.Error())
|
||||
}
|
||||
sshEntryRootSSHIsWindows := strings.Split(output, global.FSSpace)
|
||||
err = sshClient.Close()
|
||||
if err != nil {
|
||||
return "", "", errors.New("device ID gen failed - unable to close SSH client: " + err.Error())
|
||||
return "", "", errors.New("unable to close SSH client: " + err.Error())
|
||||
}
|
||||
|
||||
return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1], nil
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package syncserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/synccommon"
|
||||
)
|
||||
@@ -14,12 +14,15 @@ import (
|
||||
// GetRemoteDataFromServer prints to stdout the remote entries, mod times, folders, and deletions.
|
||||
// Lists in output are separated by FSSpace.
|
||||
// Output is meant to be captured over SSH for interpretation by the client.
|
||||
func GetRemoteDataFromServer(clientDeviceID string) {
|
||||
entryList, dirList := synccommon.WalkEntryDir()
|
||||
func GetRemoteDataFromServer(clientDeviceID string) error {
|
||||
entryList, dirList, err := synccommon.WalkEntryDir()
|
||||
if err != nil {
|
||||
return errors.New("unable to walk the entry directory: " + err.Error())
|
||||
}
|
||||
modList := synccommon.GetModTimes(entryList)
|
||||
deletionsList, err := os.ReadDir(global.ConfigDir + global.PathSeparator + "deletions")
|
||||
if err != nil {
|
||||
back.PrintError("Failed to read the deletions directory: "+err.Error(), back.ErrorRead, true)
|
||||
return errors.New("unable to read the deletions directory: " + err.Error())
|
||||
}
|
||||
|
||||
// print the current UNIX timestamp to stdout
|
||||
@@ -57,4 +60,5 @@ func GetRemoteDataFromServer(clientDeviceID string) {
|
||||
_ = os.Remove(global.ConfigDir + global.PathSeparator + "deletions" + global.PathSeparator + deletion.Name()) // error ignored; function not run from a user-facing argument and thus the error would not be visible
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user