mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-28 04:46:42 -04:00
Refactor: Split into more packages to reduce server binary size
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"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.
|
||||
@@ -17,7 +18,7 @@ func clipClearProcess(assignedContents string) {
|
||||
clearClipboard := func() {
|
||||
err := cmdClear.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to clear clipboard", ErrorClipboard, true)
|
||||
back.PrintError("Failed to clear clipboard", global.ErrorClipboard, true)
|
||||
}
|
||||
back.Exit(0)
|
||||
}
|
||||
@@ -33,7 +34,7 @@ func clipClearProcess(assignedContents string) {
|
||||
|
||||
newContents, err := cmdPaste.Output()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to read clipboard contents", ErrorClipboard, true)
|
||||
back.PrintError("Failed to read clipboard contents", global.ErrorClipboard, true)
|
||||
}
|
||||
|
||||
if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||
|
||||
+3
-19
@@ -2,17 +2,16 @@ package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
cfg, err := ini.Load(ConfigPath)
|
||||
cfg, err := ini.Load(global.ConfigPath)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to load libmutton.ini: "+err.Error(), back.ErrorRead, true)
|
||||
}
|
||||
@@ -54,21 +53,6 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin
|
||||
return config, err
|
||||
}
|
||||
|
||||
// GenDeviceIDList returns a pointer to 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 {
|
||||
// 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 &deviceIDList
|
||||
}
|
||||
|
||||
// WriteConfig writes the provided key-value pairs under the specified section headers in the libmutton.ini file.
|
||||
// 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),
|
||||
@@ -108,7 +92,7 @@ func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool
|
||||
}
|
||||
|
||||
// save to libmutton.ini
|
||||
err := cfg.SaveTo(ConfigPath)
|
||||
err := cfg.SaveTo(global.ConfigPath)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to save libmutton.ini: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
|
||||
+2
-1
@@ -9,13 +9,14 @@ import (
|
||||
steamtotp "github.com/fortis/go-steam-totp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/crypt"
|
||||
)
|
||||
|
||||
// CopyArgument copies a field from an entry to the clipboard.
|
||||
func CopyArgument(targetLocation string, field int) {
|
||||
if isFile, _ := back.TargetIsFile(targetLocation, true, 2); isFile {
|
||||
|
||||
decryptedEntry := DecryptFileToSlice(targetLocation)
|
||||
decryptedEntry := crypt.DecryptFileToSlice(targetLocation)
|
||||
var copySubject string // will store data to be copied
|
||||
|
||||
// ensure field exists in entry
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
@@ -14,7 +15,7 @@ func copyString(continuous bool, copySubject string) {
|
||||
back.WriteToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true)
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
@@ -14,7 +15,7 @@ func copyString(continuous bool, copySubject string) {
|
||||
back.WriteToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true)
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
@@ -20,13 +21,13 @@ func copyString(continuous bool, copySubject string) {
|
||||
} 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", ErrorClipboard, true)
|
||||
back.PrintError("Clipboard platform could not be determined", global.ErrorClipboard, true)
|
||||
}
|
||||
|
||||
back.WriteToStdin(cmdCopy, copySubject)
|
||||
err := cmdCopy.Run()
|
||||
if err != nil {
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true)
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
@@ -15,7 +16,7 @@ func copyString(continuous bool, copySubject string) {
|
||||
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(), ErrorClipboard, true)
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), global.ErrorClipboard, true)
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
|
||||
+5
-2
@@ -1,6 +1,9 @@
|
||||
package core
|
||||
|
||||
import "github.com/rwinkhart/go-boilerplate/back"
|
||||
import (
|
||||
"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 {
|
||||
@@ -8,7 +11,7 @@ func GetOldEntryData(targetLocation string, field int) []string {
|
||||
back.TargetIsFile(targetLocation, true, 2)
|
||||
|
||||
// read old entry data
|
||||
unencryptedEntry := DecryptFileToSlice(targetLocation)
|
||||
unencryptedEntry := crypt.DecryptFileToSlice(targetLocation)
|
||||
|
||||
// return the old entry data with all required lines present
|
||||
if field > 0 {
|
||||
|
||||
+2
-48
@@ -1,61 +1,15 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/rcw/wrappers"
|
||||
)
|
||||
|
||||
// RCWSanityCheckGen generates the RCW sanity check file for libmutton.
|
||||
func RCWSanityCheckGen(passphrase []byte) {
|
||||
err := wrappers.GenSanityCheck(ConfigDir+PathSeparator+"sanity.rcw", passphrase)
|
||||
err := wrappers.GenSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", passphrase)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to generate sanity check file: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// create EntryRoot
|
||||
err := os.MkdirAll(EntryRoot, 0700)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
|
||||
// get old device ID before its potential removal
|
||||
oldDeviceID := GetCurrentDeviceID()
|
||||
|
||||
// remove existing config directory (if it exists and not in append mode)
|
||||
if !preserveOldConfigDir {
|
||||
_, isAccessible := back.TargetIsFile(ConfigDir, false, 1)
|
||||
if isAccessible {
|
||||
err = os.RemoveAll(ConfigDir)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to remove existing config directory: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 oldDeviceID
|
||||
}
|
||||
|
||||
// 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
|
||||
var deviceID string
|
||||
if deviceIDList != nil && len(*deviceIDList) > 0 { // ensure not derferencing nil, which occurs when the devices directory does not exist
|
||||
deviceID = (*deviceIDList)[0].Name()
|
||||
} else {
|
||||
deviceID = FSMisc // indicates to server that no device ID is being replaced
|
||||
}
|
||||
return deviceID
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/crypt"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// WriteEntry writes entryData to an encrypted file at targetLocation.
|
||||
func WriteEntry(targetLocation string, entryData []byte) {
|
||||
encBytes := EncryptBytes(entryData)
|
||||
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)
|
||||
@@ -57,13 +59,13 @@ func EntryAddPrecheck(targetLocation string) uint8 {
|
||||
// ensure target location does not already exist
|
||||
_, isAccessible := back.TargetIsFile(targetLocation, false, 0)
|
||||
if isAccessible {
|
||||
back.PrintError("Target location already exists", ErrorTargetExists, false)
|
||||
back.PrintError("Target location already exists", global.ErrorTargetExists, false)
|
||||
return 1 // inform interactive clients that the target location already exists
|
||||
}
|
||||
// ensure target containing directory exists and is a directory (not a file)
|
||||
containingDir := targetLocation[:strings.LastIndex(targetLocation, PathSeparator)]
|
||||
isFile, isAccisAccessible := back.TargetIsFile(containingDir, false, 1)
|
||||
if isFile || !isAccisAccessible {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package core
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/rcw/daemon"
|
||||
"github.com/rwinkhart/rcw/wrappers"
|
||||
)
|
||||
@@ -38,7 +39,7 @@ func DecryptFileToSlice(targetLocation string) []string {
|
||||
// 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(), ErrorDecryption, true)
|
||||
back.PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), global.ErrorDecryption, true)
|
||||
}
|
||||
return strings.Split(string(decBytes), "\n")
|
||||
}
|
||||
@@ -63,8 +64,8 @@ func launchRCWDProcess() []byte {
|
||||
}
|
||||
var passphrase []byte
|
||||
for {
|
||||
passphrase = GetPassphrase("RCW Passphrase:")
|
||||
err := wrappers.RunSanityCheck(ConfigDir+PathSeparator+"sanity.rcw", passphrase)
|
||||
passphrase = global.GetPassphrase("RCW Passphrase:")
|
||||
err := wrappers.RunSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", passphrase)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package core
|
||||
package global
|
||||
|
||||
type ByteInputFetcher func(prompt string) []byte
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package core
|
||||
package global
|
||||
|
||||
import "github.com/rwinkhart/go-boilerplate/back"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package core
|
||||
package global
|
||||
|
||||
import "github.com/rwinkhart/go-boilerplate/back"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"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
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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 deviceIDList
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// create EntryRoot
|
||||
err := os.MkdirAll(EntryRoot, 0700)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
|
||||
// get old device ID before its potential removal
|
||||
oldDeviceID := GetCurrentDeviceID()
|
||||
|
||||
// remove existing config directory (if it exists and not in append mode)
|
||||
if !preserveOldConfigDir {
|
||||
_, isAccessible := back.TargetIsFile(ConfigDir, false, 1)
|
||||
if isAccessible {
|
||||
err = os.RemoveAll(ConfigDir)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to remove existing config directory: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 oldDeviceID
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package core
|
||||
package global
|
||||
|
||||
// TargetLocationFormat returns the full location of an entry (given the name) formatted for the current platform.
|
||||
func TargetLocationFormat(targetLocationIncomplete string) string {
|
||||
@@ -1,8 +1,10 @@
|
||||
//go:build windows
|
||||
|
||||
package core
|
||||
package global
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TargetLocationFormat returns the full location of an entry (given the name) formatted for the current platform.
|
||||
func TargetLocationFormat(targetLocationIncomplete string) string {
|
||||
+14
-13
@@ -8,8 +8,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/sync"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/synccommon"
|
||||
"github.com/rwinkhart/libmutton/syncserver"
|
||||
)
|
||||
|
||||
const ansiBold = "\033[1m"
|
||||
@@ -37,38 +38,38 @@ func main() {
|
||||
case "fetch":
|
||||
// print all information needed for syncing to stdout for interpretation by the client
|
||||
// stdin[0] is expected to be the device ID
|
||||
sync.GetRemoteDataFromServer(stdin[0])
|
||||
syncserver.GetRemoteDataFromServer(stdin[0])
|
||||
case "rename":
|
||||
// move an entry to a new location before using fallthrough to add its previous iteration to the deletions directory
|
||||
// stdin[0] is evaluated after fallthrough
|
||||
// stdin[1] is expected to be the OLD incomplete target location with FSPath representing path separators - Always pass in UNIX format
|
||||
// stdin[2] is expected to be the NEW incomplete target location with FSPath representing path separators - Always pass in UNIX format
|
||||
sync.RenameLocal(strings.ReplaceAll(stdin[1], core.FSPath, "/"), strings.ReplaceAll(stdin[2], core.FSPath, "/"), true)
|
||||
synccommon.RenameLocal(strings.ReplaceAll(stdin[1], global.FSPath, "/"), strings.ReplaceAll(stdin[2], global.FSPath, "/"), true)
|
||||
fallthrough // fallthrough to add the old entry to the deletions directory
|
||||
case "shear":
|
||||
// shear an entry from the server and add it to the deletions directory
|
||||
// stdin[0] is expected to be the device ID
|
||||
// stdin[1] is expected to be the incomplete target location with FSPath representing path separators - Always pass in UNIX format
|
||||
sync.ShearLocal(strings.ReplaceAll(stdin[1], core.FSPath, "/"), stdin[0])
|
||||
synccommon.ShearLocal(strings.ReplaceAll(stdin[1], global.FSPath, "/"), stdin[0])
|
||||
case "addfolder":
|
||||
// add a new folder to the server
|
||||
// stdin[0] is expected to be the incomplete target location with FSPath representing path separators - Always pass in UNIX format
|
||||
sync.AddFolderLocal(strings.ReplaceAll(stdin[0], core.FSPath, "/"))
|
||||
synccommon.AddFolderLocal(strings.ReplaceAll(stdin[0], global.FSPath, "/"))
|
||||
case "register":
|
||||
// register a new device ID
|
||||
// stdin[0] is expected to be the device ID
|
||||
// stdin[1] is expected to be the old device ID (for removal)
|
||||
fileToClose, _ := os.OpenFile(core.ConfigDir+core.PathSeparator+"devices"+core.PathSeparator+stdin[0], os.O_CREATE|os.O_WRONLY, 0600) // errors ignored; failure unlikely to occur if init was successful; "register" is not a user-facing argument and thus the error would not be visible
|
||||
fileToClose, _ := os.OpenFile(global.ConfigDir+global.PathSeparator+"devices"+global.PathSeparator+stdin[0], os.O_CREATE|os.O_WRONLY, 0600) // errors ignored; failure unlikely to occur if init was successful; "register" is not a user-facing argument and thus the error would not be visible
|
||||
_ = fileToClose.Close()
|
||||
if stdin[1] != core.FSMisc { // sync.FSMisc is used to indicate that no device ID is being replaced
|
||||
_ = os.RemoveAll(core.ConfigDir + core.PathSeparator + "devices" + core.PathSeparator + stdin[1])
|
||||
if stdin[1] != global.FSMisc { // sync.FSMisc is used to indicate that no device ID is being replaced
|
||||
_ = os.RemoveAll(global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + stdin[1])
|
||||
}
|
||||
// print EntryRoot and bool indicating OS type to stdout for client to store in config
|
||||
fmt.Print(core.EntryRoot + core.FSSpace + strconv.FormatBool(core.IsWindows))
|
||||
fmt.Print(global.EntryRoot + global.FSSpace + strconv.FormatBool(global.IsWindows))
|
||||
case "init":
|
||||
// create the necessary directories for libmuttonserver to function
|
||||
core.DirInit(false)
|
||||
_ = os.MkdirAll(core.ConfigDir+core.PathSeparator+"deletions", 0700) // error ignored; failure would have occurred by this point in core.DirInit
|
||||
global.DirInit(false)
|
||||
_ = os.MkdirAll(global.ConfigDir+global.PathSeparator+"deletions", 0700) // error ignored; failure would have occurred by this point in core.DirInit
|
||||
fmt.Println("libmuttonserver directories initialized")
|
||||
case "version":
|
||||
versionServer()
|
||||
@@ -117,5 +118,5 @@ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.` + "\n\n---------------------------------------------------------")
|
||||
fmt.Print(ansiBold + "\n\n libmuttonserver" + back.AnsiReset + " Version " + core.LibmuttonVersion + "\n\n Copyright (c) 2024-2025: Randall Winkhart" + "\n\n")
|
||||
fmt.Print(ansiBold + "\n\n libmuttonserver" + back.AnsiReset + " Version " + global.LibmuttonVersion + "\n\n Copyright (c) 2024-2025: Randall Winkhart" + "\n\n")
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package sync
|
||||
|
||||
import "github.com/rwinkhart/libmutton/core"
|
||||
|
||||
var RootLength = len(core.EntryRoot) // length of core.EntryRoot string
|
||||
@@ -1,4 +1,4 @@
|
||||
package sync
|
||||
package syncclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -10,17 +10,12 @@ import (
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/synccommon"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/knownhosts"
|
||||
)
|
||||
|
||||
// ANSI color constants used only in this file
|
||||
const (
|
||||
ansiDelete = "\033[38;5;1m"
|
||||
ansiDownload = "\033[38;5;2m"
|
||||
ansiUpload = "\033[38;5;4m"
|
||||
)
|
||||
|
||||
// GetSSHClient returns an SSH client connection to the server (also returns the remote EntryRoot and an indicator of the server's OS).
|
||||
// Only supports key-based authentication (passphrases are supported for CLI-based implementations).
|
||||
func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
@@ -70,7 +65,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
if keyFileProtected != "true" {
|
||||
parsedKey, err = ssh.ParsePrivateKey(key)
|
||||
} else {
|
||||
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, core.GetPassphrase("Enter passphrase for your SSH keyfile:"))
|
||||
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, global.GetPassphrase("Enter passphrase for your SSH keyfile:"))
|
||||
}
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to parse private key: "+keyFile, back.ErrorRead, true)
|
||||
@@ -78,7 +73,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
|
||||
// read known hosts file
|
||||
var hostKeyCallback ssh.HostKeyCallback
|
||||
hostKeyCallback, err = knownhosts.New(back.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts")
|
||||
hostKeyCallback, err = knownhosts.New(back.Home + global.PathSeparator + ".ssh" + global.PathSeparator + "known_hosts")
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to read known hosts file: "+err.Error(), back.ErrorRead, true)
|
||||
}
|
||||
@@ -96,7 +91,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
// connect to SSH server
|
||||
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to connect to remote server: "+err.Error(), core.ErrorServerConnection, false) // do not crash/close interactive clients
|
||||
back.PrintError("Sync failed - Unable to connect to remote server: "+err.Error(), global.ErrorServerConnection, false) // do not crash/close interactive clients
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
@@ -108,7 +103,7 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string {
|
||||
// create a session
|
||||
sshSession, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to establish SSH session: "+err.Error(), core.ErrorServerConnection, true)
|
||||
back.PrintError("Sync failed - Unable to establish SSH session: "+err.Error(), global.ErrorServerConnection, true)
|
||||
}
|
||||
|
||||
// provide stdin data for session
|
||||
@@ -118,7 +113,7 @@ 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(), core.ErrorSyncProcess, true)
|
||||
back.PrintError("Sync failed - Unable to run SSH command: "+err.Error(), global.ErrorSyncProcess, true)
|
||||
}
|
||||
|
||||
// convert the output to a string and remove leading/trailing whitespace
|
||||
@@ -131,8 +126,8 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string {
|
||||
// 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) {
|
||||
// get remote output over SSH
|
||||
deviceIDList := core.GenDeviceIDList(true)
|
||||
if len(*deviceIDList) == 0 {
|
||||
deviceIDList := global.GenDeviceIDList(true)
|
||||
if len(deviceIDList) == 0 {
|
||||
if manualSync {
|
||||
back.PrintError("Sync failed - No device ID found", back.ErrorTargetNotFound, true)
|
||||
} else {
|
||||
@@ -140,23 +135,23 @@ func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string
|
||||
}
|
||||
}
|
||||
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 := GetSSHOutput(sshClient, "libmuttonserver fetch", (deviceIDList)[0].Name())
|
||||
|
||||
// split output into slice based on occurrences of FSSpace
|
||||
outputSlice := strings.Split(output, core.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", core.ErrorSyncProcess, true)
|
||||
back.PrintError("Sync failed - Unable to fetch remote data; server returned an unexpected response", global.ErrorSyncProcess, true)
|
||||
}
|
||||
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)
|
||||
}
|
||||
entries := strings.Split(outputSlice[1], core.FSMisc)[1:]
|
||||
modsStrings := strings.Split(outputSlice[2], core.FSMisc)[1:]
|
||||
folders := strings.Split(outputSlice[3], core.FSMisc)[1:]
|
||||
deletions := strings.Split(outputSlice[4], core.FSMisc)[1:]
|
||||
entries := strings.Split(outputSlice[1], global.FSMisc)[1:]
|
||||
modsStrings := strings.Split(outputSlice[2], global.FSMisc)[1:]
|
||||
folders := strings.Split(outputSlice[3], global.FSMisc)[1:]
|
||||
deletions := strings.Split(outputSlice[4], global.FSMisc)[1:]
|
||||
|
||||
// convert the mod times to int64
|
||||
var mods []int64
|
||||
@@ -181,10 +176,10 @@ func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string
|
||||
// getLocalData returns a map of local entries to their modification times.
|
||||
func getLocalData() map[string]int64 {
|
||||
// get a list of all entries
|
||||
entries, _ := WalkEntryDir()
|
||||
entries, _ := synccommon.WalkEntryDir()
|
||||
|
||||
// get a list of all entry modification times
|
||||
modList := getModTimes(entries)
|
||||
modList := synccommon.GetModTimes(entries)
|
||||
|
||||
// map the entries to their modification times
|
||||
entryModMap := make(map[string]int64)
|
||||
@@ -210,12 +205,12 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
// 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(), core.ErrorServerConnection, true)
|
||||
back.PrintError("Sync failed - Unable to establish SFTP session: "+err.Error(), global.ErrorServerConnection, true)
|
||||
}
|
||||
defer func(sftpClient *sftp.Client) {
|
||||
err = sftpClient.Close()
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to close SFTP client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
back.PrintError("Sync failed - Unable to close SFTP client: "+err.Error(), global.ErrorServerConnection, true)
|
||||
}
|
||||
}(sftpClient)
|
||||
|
||||
@@ -224,7 +219,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
for _, entryName := range downloadList {
|
||||
filesTransferred = true // set a flag to indicate that files have been downloaded (used to determine whether to print a gap between download and upload messages)
|
||||
|
||||
fmt.Println("Downloading " + ansiDownload + entryName + back.AnsiReset)
|
||||
fmt.Println("Downloading " + synccommon.AnsiDownload + entryName + back.AnsiReset)
|
||||
|
||||
// store path to remote entry
|
||||
remoteEntryFullPath := targetLocationFormatSFTP(entryName, sshEntryRoot, sshIsWindows)
|
||||
@@ -245,7 +240,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
}
|
||||
|
||||
// store path to local entry
|
||||
localEntryFullPath := core.TargetLocationFormat(entryName)
|
||||
localEntryFullPath := global.TargetLocationFormat(entryName)
|
||||
|
||||
// create local file
|
||||
var localFile *os.File
|
||||
@@ -257,7 +252,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
// download the file
|
||||
_, err = remoteFile.WriteTo(localFile)
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to download remote file: "+err.Error(), core.ErrorSyncProcess, true)
|
||||
back.PrintError("Sync failed - Unable to download remote file: "+err.Error(), global.ErrorSyncProcess, true)
|
||||
}
|
||||
|
||||
// close the files
|
||||
@@ -277,10 +272,10 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
for _, entryName := range uploadList {
|
||||
filesTransferred = true // set a flag to indicate that files have been uploaded (used to determine whether to print a gap between upload and sync complete messages)
|
||||
|
||||
fmt.Println("Uploading " + ansiUpload + entryName + back.AnsiReset)
|
||||
fmt.Println("Uploading " + synccommon.AnsiUpload + entryName + back.AnsiReset)
|
||||
|
||||
// store path to local entry
|
||||
localEntryFullPath := core.TargetLocationFormat(entryName)
|
||||
localEntryFullPath := global.TargetLocationFormat(entryName)
|
||||
|
||||
// save modification time of local file
|
||||
var fileInfo os.FileInfo
|
||||
@@ -310,7 +305,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
// upload the file
|
||||
_, err = localFile.WriteTo(remoteFile)
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to upload local file: "+err.Error(), core.ErrorSyncProcess, true)
|
||||
back.PrintError("Sync failed - Unable to upload local file: "+err.Error(), global.ErrorSyncProcess, true)
|
||||
}
|
||||
|
||||
// close the files
|
||||
@@ -320,7 +315,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(), core.ErrorSyncProcess, true)
|
||||
back.PrintError("Sync failed - Unable to set permissions on remote file: "+err.Error(), global.ErrorSyncProcess, true)
|
||||
}
|
||||
|
||||
// set the modification time of the remote file to match the value saved from the local file (from before the upload)
|
||||
@@ -344,23 +339,23 @@ func syncLists(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows, timeSyn
|
||||
if remoteModTime, present := remoteEntryModMap[entry]; present {
|
||||
// entry exists on both client and server, compare mod times
|
||||
if remoteModTime > localModTime {
|
||||
fmt.Println(ansiDownload+entry+back.AnsiReset, "is newer on server, adding to download list")
|
||||
fmt.Println(synccommon.AnsiDownload+entry+back.AnsiReset, "is newer on server, adding to download list")
|
||||
downloadList = append(downloadList, entry)
|
||||
} else if remoteModTime < localModTime {
|
||||
fmt.Println(ansiUpload+entry+back.AnsiReset, "is newer on client, adding to upload list")
|
||||
fmt.Println(synccommon.AnsiUpload+entry+back.AnsiReset, "is newer on client, adding to upload list")
|
||||
uploadList = append(uploadList, entry)
|
||||
}
|
||||
// remove entry from remoteEntryModMap (process of elimination)
|
||||
delete(remoteEntryModMap, entry)
|
||||
} else {
|
||||
fmt.Println(ansiUpload+entry+back.AnsiReset, "does not exist on server, adding to upload list")
|
||||
fmt.Println(synccommon.AnsiUpload+entry+back.AnsiReset, "does not exist on server, adding to upload list")
|
||||
uploadList = append(uploadList, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// iterate over remaining entries in remoteEntryModMap
|
||||
for entry := range remoteEntryModMap {
|
||||
fmt.Println(ansiDownload+entry+back.AnsiReset, "does not exist on client, adding to download list")
|
||||
fmt.Println(synccommon.AnsiDownload+entry+back.AnsiReset, "does not exist on client, adding to download list")
|
||||
downloadList = append(downloadList, entry)
|
||||
}
|
||||
|
||||
@@ -386,8 +381,8 @@ func deletionSync(deletions []string) {
|
||||
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(ansiDelete+deletion+back.AnsiReset, "has been sheared, removing locally (if it exists)")
|
||||
err := os.RemoveAll(core.TargetLocationFormat(deletion))
|
||||
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)
|
||||
}
|
||||
@@ -402,7 +397,7 @@ func deletionSync(deletions []string) {
|
||||
func folderSync(folders []string) {
|
||||
for _, folder := range folders {
|
||||
// store the full local path of the folder
|
||||
folderFullPath := core.TargetLocationFormat(folder)
|
||||
folderFullPath := global.TargetLocationFormat(folder)
|
||||
|
||||
// check if folder already exists
|
||||
isFile, isAccessible := back.TargetIsFile(folderFullPath, false, 1)
|
||||
@@ -413,7 +408,7 @@ func folderSync(folders []string) {
|
||||
back.PrintError("Sync failed - Failed to create folder ("+folder+"): "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
} else if isFile {
|
||||
back.PrintError("Sync failed - Failed to create folder ("+folder+") - A file with the same name already exists", core.ErrorTargetExists, true)
|
||||
back.PrintError("Sync failed - Failed to create folder ("+folder+") - A file with the same name already exists", global.ErrorTargetExists, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -430,7 +425,7 @@ func RunJob(manualSync, returnLists bool) [3][]string {
|
||||
defer func(sshClient *ssh.Client) {
|
||||
err := sshClient.Close()
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||
}
|
||||
}(sshClient)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package sync
|
||||
package syncclient
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// DeviceIDGen generates a new client device ID and registers it with the server (will replace existing one).
|
||||
@@ -22,14 +23,14 @@ func DeviceIDGen(oldDeviceID string) (string, string) {
|
||||
newDeviceID := deviceIDPrefix + "-" + deviceIDSuffix
|
||||
|
||||
// create new device ID file (locally)
|
||||
fileToClose, err := os.OpenFile(core.ConfigDir+core.PathSeparator+"devices"+core.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 {
|
||||
back.PrintError("Failed to create local device ID file: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
_ = 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(core.ConfigDir + core.PathSeparator + "devices" + core.PathSeparator + oldDeviceID)
|
||||
err = os.RemoveAll(global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + oldDeviceID)
|
||||
if err != nil {
|
||||
back.PrintError("Failed to remove old device ID file (locally): "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
@@ -38,10 +39,10 @@ func DeviceIDGen(oldDeviceID string) (string, string) {
|
||||
// also removes the old device ID file (remotely)
|
||||
// manualSync is true so the user is alerted if device ID registration fails
|
||||
sshClient, _, _ := GetSSHClient(true)
|
||||
sshEntryRootSSHIsWindows := strings.Split(GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), core.FSSpace)
|
||||
sshEntryRootSSHIsWindows := strings.Split(GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), global.FSSpace)
|
||||
err = sshClient.Close()
|
||||
if err != nil {
|
||||
back.PrintError("Init failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
back.PrintError("Init failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||
}
|
||||
|
||||
return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1]
|
||||
@@ -1,16 +1,17 @@
|
||||
package sync
|
||||
package syncclient
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/synccommon"
|
||||
)
|
||||
|
||||
// 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 := ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client
|
||||
deviceID, isDir := synccommon.ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client
|
||||
|
||||
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
|
||||
@@ -22,12 +23,12 @@ func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) {
|
||||
}
|
||||
|
||||
// call the server to remotely shear the target and add it to the deletions list
|
||||
GetSSHOutput(sshClient, "libmuttonserver shear", deviceID+"\n"+strings.ReplaceAll(targetLocationIncomplete, core.PathSeparator, core.FSPath))
|
||||
GetSSHOutput(sshClient, "libmuttonserver shear", deviceID+"\n"+strings.ReplaceAll(targetLocationIncomplete, global.PathSeparator, global.FSPath))
|
||||
|
||||
// close the SSH client
|
||||
err := sshClient.Close()
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,23 +38,23 @@ func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) {
|
||||
// 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) {
|
||||
RenameLocal(oldLocationIncomplete, newLocationIncomplete, false) // move the target on the local system
|
||||
synccommon.RenameLocal(oldLocationIncomplete, newLocationIncomplete, false) // move the target on the local system
|
||||
|
||||
deviceIDList := core.GenDeviceIDList(true)
|
||||
if !forceOffline && len(*deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
||||
deviceIDList := global.GenDeviceIDList(true)
|
||||
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, _, _ := GetSSHClient(false)
|
||||
|
||||
// call the server to move the target on the remote system and add the old target to the deletions list
|
||||
GetSSHOutput(sshClient, "libmuttonserver rename",
|
||||
(*deviceIDList)[0].Name()+"\n"+
|
||||
strings.ReplaceAll(oldLocationIncomplete, core.PathSeparator, core.FSPath)+"\n"+
|
||||
strings.ReplaceAll(newLocationIncomplete, core.PathSeparator, core.FSPath))
|
||||
(deviceIDList)[0].Name()+"\n"+
|
||||
strings.ReplaceAll(oldLocationIncomplete, global.PathSeparator, global.FSPath)+"\n"+
|
||||
strings.ReplaceAll(newLocationIncomplete, global.PathSeparator, global.FSPath))
|
||||
|
||||
// close the SSH client
|
||||
err := sshClient.Close()
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,20 +64,20 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string,
|
||||
// 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) {
|
||||
AddFolderLocal(targetLocationIncomplete) // add the folder on the local system
|
||||
synccommon.AddFolderLocal(targetLocationIncomplete) // add the folder on the local system
|
||||
|
||||
deviceIDList := core.GenDeviceIDList(true)
|
||||
if !forceOffline && len(*deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
||||
deviceIDList := global.GenDeviceIDList(true)
|
||||
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, _, _ := GetSSHClient(false)
|
||||
|
||||
// call the server to create the folder remotely
|
||||
GetSSHOutput(sshClient, "libmuttonserver addfolder", strings.ReplaceAll(targetLocationIncomplete, core.PathSeparator, core.FSPath)) // call the server to create the folder remotely
|
||||
GetSSHOutput(sshClient, "libmuttonserver addfolder", strings.ReplaceAll(targetLocationIncomplete, global.PathSeparator, global.FSPath)) // call the server to create the folder remotely
|
||||
|
||||
// close the SSH client
|
||||
err := sshClient.Close()
|
||||
if err != nil {
|
||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package sync
|
||||
package synccommon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,14 +6,23 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// getModTimes returns a list of all entry modification times.
|
||||
func getModTimes(entryList []string) []int64 {
|
||||
// ANSI color constants used only in this file
|
||||
const (
|
||||
AnsiDelete = "\033[38;5;1m"
|
||||
AnsiDownload = "\033[38;5;2m"
|
||||
AnsiUpload = "\033[38;5;4m"
|
||||
)
|
||||
|
||||
var RootLength = len(global.EntryRoot) // length of global.EntryRoot string
|
||||
|
||||
// GetModTimes returns a list of all entry modification times.
|
||||
func GetModTimes(entryList []string) []int64 {
|
||||
var modList []int64
|
||||
for _, file := range entryList {
|
||||
modTime, _ := os.Stat(core.TargetLocationFormat(file))
|
||||
modTime, _ := os.Stat(global.TargetLocationFormat(file))
|
||||
modList = append(modList, modTime.ModTime().Unix())
|
||||
}
|
||||
|
||||
@@ -32,13 +41,13 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool)
|
||||
onServer = true
|
||||
}
|
||||
|
||||
deviceIDList := core.GenDeviceIDList(true)
|
||||
deviceIDList := global.GenDeviceIDList(true)
|
||||
|
||||
// add the sheared target (incomplete, vanity) to the deletions list (if running on a server)
|
||||
if onServer {
|
||||
for _, device := range *deviceIDList {
|
||||
for _, device := range deviceIDList {
|
||||
if device.Name() != clientDeviceID {
|
||||
fileToClose, err := os.OpenFile(core.ConfigDir+core.PathSeparator+"deletions"+core.PathSeparator+device.Name()+core.FSSpace+strings.ReplaceAll(targetLocationIncomplete, "/", core.FSPath), os.O_CREATE|os.O_WRONLY, 0600)
|
||||
fileToClose, err := os.OpenFile(global.ConfigDir+global.PathSeparator+"deletions"+global.PathSeparator+device.Name()+global.FSSpace+strings.ReplaceAll(targetLocationIncomplete, "/", global.FSPath), os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
// do not print error as there is currently no way of seeing server-side errors
|
||||
// failure to add the target to the deletions list will exit the program and result in a client re-uploading the target (non-critical)
|
||||
@@ -50,7 +59,7 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool)
|
||||
}
|
||||
|
||||
// get the full targetLocation path and remove the target
|
||||
targetLocationComplete := core.TargetLocationFormat(targetLocationIncomplete)
|
||||
targetLocationComplete := global.TargetLocationFormat(targetLocationIncomplete)
|
||||
var isFile 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)
|
||||
@@ -60,8 +69,8 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool)
|
||||
back.PrintError("Failed to remove local target: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
|
||||
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
|
||||
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 "", true
|
||||
|
||||
@@ -72,8 +81,8 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool)
|
||||
// This function should only be used directly by the server binary.
|
||||
func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldLocationExists bool) {
|
||||
// get full paths for both locations
|
||||
oldLocation := core.TargetLocationFormat(oldLocationIncomplete)
|
||||
newLocation := core.TargetLocationFormat(newLocationIncomplete)
|
||||
oldLocation := global.TargetLocationFormat(oldLocationIncomplete)
|
||||
newLocation := global.TargetLocationFormat(newLocationIncomplete)
|
||||
|
||||
if verifyOldLocationExists {
|
||||
back.TargetIsFile(oldLocation, true, 0)
|
||||
@@ -82,7 +91,7 @@ func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldL
|
||||
// ensure newLocation does not exist
|
||||
_, isAccessible := back.TargetIsFile(newLocation, false, 0)
|
||||
if isAccessible {
|
||||
back.PrintError("\""+newLocation+"\" already exists", core.ErrorTargetExists, true)
|
||||
back.PrintError("\""+newLocation+"\" already exists", global.ErrorTargetExists, true)
|
||||
}
|
||||
|
||||
// rename oldLocation to newLocation
|
||||
@@ -98,11 +107,11 @@ func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldL
|
||||
// This function should only be used directly by the server binary.
|
||||
func AddFolderLocal(targetLocationIncomplete string) {
|
||||
// get the full targetLocation path and create the target
|
||||
targetLocationComplete := core.TargetLocationFormat(targetLocationIncomplete)
|
||||
targetLocationComplete := global.TargetLocationFormat(targetLocationIncomplete)
|
||||
err := os.Mkdir(targetLocationComplete, 0700)
|
||||
if err != nil {
|
||||
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 {
|
||||
back.PrintError("Failed to create directory: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package sync
|
||||
package synccommon
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists).
|
||||
@@ -19,7 +19,7 @@ func WalkEntryDir() ([]string, []string) {
|
||||
var dirList []string
|
||||
|
||||
// walk entry directory
|
||||
_ = filepath.WalkDir(core.EntryRoot,
|
||||
_ = filepath.WalkDir(global.EntryRoot,
|
||||
func(fullPath string, entry fs.DirEntry, err error) error {
|
||||
|
||||
// check for errors encountered while walking directory
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package sync
|
||||
package synccommon
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
)
|
||||
|
||||
// WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists).
|
||||
@@ -20,7 +20,7 @@ func WalkEntryDir() ([]string, []string) {
|
||||
var dirList []string
|
||||
|
||||
// walk entry directory
|
||||
_ = filepath.WalkDir(core.EntryRoot,
|
||||
_ = filepath.WalkDir(global.EntryRoot,
|
||||
func(fullPath string, entry fs.DirEntry, err error) error {
|
||||
|
||||
// check for errors encountered while walking directory
|
||||
@@ -1,4 +1,4 @@
|
||||
package sync
|
||||
package syncserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,16 +7,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/synccommon"
|
||||
)
|
||||
|
||||
// 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 := WalkEntryDir()
|
||||
modList := getModTimes(entryList)
|
||||
deletionsList, err := os.ReadDir(core.ConfigDir + core.PathSeparator + "deletions")
|
||||
entryList, dirList := synccommon.WalkEntryDir()
|
||||
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)
|
||||
}
|
||||
@@ -26,34 +27,34 @@ func GetRemoteDataFromServer(clientDeviceID string) {
|
||||
|
||||
// print the lists to stdout
|
||||
// entry list
|
||||
fmt.Print(core.FSSpace)
|
||||
fmt.Print(global.FSSpace)
|
||||
for _, entry := range entryList {
|
||||
fmt.Print(core.FSMisc + entry)
|
||||
fmt.Print(global.FSMisc + entry)
|
||||
}
|
||||
|
||||
// modification time list
|
||||
fmt.Print(core.FSSpace)
|
||||
fmt.Print(global.FSSpace)
|
||||
for _, mod := range modList {
|
||||
fmt.Print(core.FSMisc)
|
||||
fmt.Print(global.FSMisc)
|
||||
fmt.Print(mod)
|
||||
}
|
||||
|
||||
// directory/folder list
|
||||
fmt.Print(core.FSSpace)
|
||||
fmt.Print(global.FSSpace)
|
||||
for _, dir := range dirList {
|
||||
fmt.Print(core.FSMisc + dir)
|
||||
fmt.Print(global.FSMisc + dir)
|
||||
}
|
||||
|
||||
// deletions list
|
||||
fmt.Print(core.FSSpace)
|
||||
fmt.Print(global.FSSpace)
|
||||
for _, deletion := range deletionsList {
|
||||
// print deletion if it is relevant to the current client device
|
||||
affectedIDTargetLocationIncomplete := strings.Split(deletion.Name(), core.FSSpace)
|
||||
affectedIDTargetLocationIncomplete := strings.Split(deletion.Name(), global.FSSpace)
|
||||
if affectedIDTargetLocationIncomplete[0] == clientDeviceID {
|
||||
fmt.Print(core.FSMisc + strings.ReplaceAll(affectedIDTargetLocationIncomplete[1], core.FSPath, "/"))
|
||||
fmt.Print(global.FSMisc + strings.ReplaceAll(affectedIDTargetLocationIncomplete[1], global.FSPath, "/"))
|
||||
|
||||
// assume successful client deletion and remove deletions file (if assumption is somehow false, worst case scenario is that the client will re-upload the deleted entry)
|
||||
_ = os.Remove(core.ConfigDir + core.PathSeparator + "deletions" + core.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
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@ These are as follows:
|
||||
- `termux`: Allows creating an Android binary that can interact with the Termux clipboard (for Android)
|
||||
|
||||
## Required Global Variable Manipulation
|
||||
libmutton provides a `GetPassphrase` global variable that all clients must set. This approach allows for different types of clients (CLI, GUI, TUI) to prompt for the passphrase in the most appropriate way.
|
||||
libmutton provides a `global.GetPassphrase` global variable that all clients must set. This approach allows for different types of clients (CLI, GUI, TUI) to prompt for the passphrase in the most appropriate way.
|
||||
|
||||
## Required Arguments
|
||||
- `clipclear`: Should be accepted by all non-interactive CLI libmutton implementations (not required for interactive GUI/TUI implementations). In order to clear the clipboard on a timer, non-interactive libmutton-based password managers call another instance of their executable with the `clipclear` argument (e.g. `mutn clipclear`) with the intended clipboard contents provided via STDIN. If after 30 seconds the clipboard contents have not changed, they are cleared. Please accept a `clipclear` argument that calls `core.ClipClearArgument()`.
|
||||
|
||||
Reference in New Issue
Block a user