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
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user