mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-28 04:46:42 -04:00
Add offline mode option to libmutton.ini
This commit is contained in:
@@ -21,6 +21,7 @@ func loadConfig() (*ini.File, error) {
|
|||||||
// 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.
|
||||||
// Requires: valuesRequested (a slice of length 2 arrays each containing a section and a key name).
|
// Requires: valuesRequested (a slice of length 2 arrays each containing a section and a key name).
|
||||||
// Returns: config (slice of values for the specified keys).
|
// Returns: config (slice of values for the specified keys).
|
||||||
|
// If requesting SSH config, request "LIBMUTTON/offlineMode" first to avoid errors.
|
||||||
func ParseConfig(valuesRequested [][2]string) ([]string, error) {
|
func ParseConfig(valuesRequested [][2]string) ([]string, error) {
|
||||||
cfg, err := loadConfig()
|
cfg, err := loadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -35,6 +36,11 @@ func ParseConfig(valuesRequested [][2]string) ([]string, error) {
|
|||||||
return nil, fmt.Errorf("unable to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
|
return nil, fmt.Errorf("unable to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
|
||||||
}
|
}
|
||||||
config = append(config, value)
|
config = append(config, value)
|
||||||
|
|
||||||
|
// notify requester immediately if in offline mode
|
||||||
|
if pair[0] == "LIBMUTTON" && pair[1] == "offlineMode" && value == "true" {
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, err
|
return config, err
|
||||||
|
|||||||
+2
-1
@@ -47,6 +47,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
|
|||||||
err = cfg.WriteConfig(append(
|
err = cfg.WriteConfig(append(
|
||||||
clientSpecificIniData,
|
clientSpecificIniData,
|
||||||
[][3]string{
|
[][3]string{
|
||||||
|
{"LIBMUTTON", "offlineMode", "false"},
|
||||||
{"LIBMUTTON", "sshUser", sshUser},
|
{"LIBMUTTON", "sshUser", sshUser},
|
||||||
{"LIBMUTTON", "sshIP", sshIP},
|
{"LIBMUTTON", "sshIP", sshIP},
|
||||||
{"LIBMUTTON", "sshPort", sshPort},
|
{"LIBMUTTON", "sshPort", sshPort},
|
||||||
@@ -74,7 +75,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
|
|||||||
}
|
}
|
||||||
// write config file
|
// write config file
|
||||||
if len(clientSpecificIniData) > 0 { // TODO test passing empty clientSpecificIniData
|
if len(clientSpecificIniData) > 0 { // TODO test passing empty clientSpecificIniData
|
||||||
err = cfg.WriteConfig(clientSpecificIniData, nil, false)
|
err = cfg.WriteConfig(append(clientSpecificIniData, [][3]string{{"LIBMUTTON", "offlineMode", "true"}}...), nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("unable to write config file: " + err.Error())
|
return errors.New("unable to write config file: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-26
@@ -17,13 +17,22 @@ import (
|
|||||||
"golang.org/x/crypto/ssh/knownhosts"
|
"golang.org/x/crypto/ssh/knownhosts"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSSHClient returns an SSH client connection to the server (also returns the remote EntryRoot and an indicator of the server's OS).
|
// GetSSHClient
|
||||||
|
// Returns:
|
||||||
|
// sshClient,
|
||||||
|
// offlineMode (whether the client is in offline mode).
|
||||||
|
// sshIsWindows (whether the remote server is running Windows),
|
||||||
|
// sshEntryRoot (the root directory for entries on the remote server),
|
||||||
// Only supports key-based authentication (passphrases are supported for CLI-based implementations).
|
// Only supports key-based authentication (passphrases are supported for CLI-based implementations).
|
||||||
func GetSSHClient() (*ssh.Client, string, bool, error) {
|
func GetSSHClient() (*ssh.Client, bool, bool, string, error) {
|
||||||
// get SSH config info, exit if not configured
|
// get SSH config info
|
||||||
sshUserConfig, err := cfg.ParseConfig([][2]string{{"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshIsWindows"}})
|
sshUserConfig, err := cfg.ParseConfig([][2]string{{"LIBMUTTON", "offlineMode"}, {"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshIsWindows"}})
|
||||||
|
if len(sshUserConfig) == 1 {
|
||||||
|
// offline mode is enabled
|
||||||
|
return nil, true, false, "", nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", false, errors.New("unable to parse SSH config: " + err.Error())
|
return nil, false, false, "", errors.New("unable to parse SSH config: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var user, ip, port, keyFile, keyFileProtected, entryRoot string
|
var user, ip, port, keyFile, keyFileProtected, entryRoot string
|
||||||
@@ -45,7 +54,7 @@ func GetSSHClient() (*ssh.Client, string, bool, error) {
|
|||||||
case 6:
|
case 6:
|
||||||
isWindows, err = strconv.ParseBool(key)
|
isWindows, err = strconv.ParseBool(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", false, errors.New("unable to parse server OS type: " + err.Error())
|
return nil, false, false, "", errors.New("unable to parse server OS type: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +62,7 @@ func GetSSHClient() (*ssh.Client, string, bool, error) {
|
|||||||
// read private key
|
// read private key
|
||||||
key, err := os.ReadFile(keyFile)
|
key, err := os.ReadFile(keyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", false, errors.New("unable to read private key: " + keyFile)
|
return nil, false, false, "", errors.New("unable to read private key: " + keyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse private key
|
// parse private key
|
||||||
@@ -64,14 +73,14 @@ func GetSSHClient() (*ssh.Client, string, bool, error) {
|
|||||||
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, global.GetPassphrase("Enter passphrase for your SSH keyfile:"))
|
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, global.GetPassphrase("Enter passphrase for your SSH keyfile:"))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", false, errors.New("unable to parse private key: " + keyFile)
|
return nil, false, false, "", errors.New("unable to parse private key: " + keyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// read known hosts file
|
// read known hosts file
|
||||||
var hostKeyCallback ssh.HostKeyCallback
|
var hostKeyCallback ssh.HostKeyCallback
|
||||||
hostKeyCallback, err = knownhosts.New(back.Home + global.PathSeparator + ".ssh" + global.PathSeparator + "known_hosts")
|
hostKeyCallback, err = knownhosts.New(back.Home + global.PathSeparator + ".ssh" + global.PathSeparator + "known_hosts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", false, errors.New("unable to read known hosts file: " + err.Error())
|
return nil, false, false, "", errors.New("unable to read known hosts file: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// configure SSH client
|
// configure SSH client
|
||||||
@@ -87,10 +96,10 @@ func GetSSHClient() (*ssh.Client, string, bool, error) {
|
|||||||
// connect to SSH server
|
// connect to SSH server
|
||||||
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", false, errors.New("unable to connect to remote server: " + err.Error())
|
return nil, false, false, "", errors.New("unable to connect to remote server: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshClient, entryRoot, isWindows, nil
|
return sshClient, false, isWindows, entryRoot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -119,18 +128,14 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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, error) {
|
func getRemoteDataFromClient(sshClient *ssh.Client) (map[string]int64, []string, []string, int64, int64, error) {
|
||||||
// get remote output over SSH
|
// get remote output over SSH
|
||||||
deviceIDList, err := global.GenDeviceIDList()
|
deviceIDList, err := global.GenDeviceIDList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, 0, 0, err
|
return nil, nil, nil, 0, 0, err
|
||||||
}
|
}
|
||||||
if len(deviceIDList) == 0 {
|
if len(deviceIDList) == 0 {
|
||||||
if manualSync {
|
return nil, nil, nil, 0, 0, errors.New("no device ID found")
|
||||||
return nil, nil, nil, 0, 0, errors.New("no device ID found")
|
|
||||||
} else {
|
|
||||||
back.Exit(0) // exit silently if the sync job was called automatically, as the user may just be in offline mode
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
clientTime := time.Now().Unix() // get client time now to avoid accuracy issues caused by unpredictable sync time
|
clientTime := time.Now().Unix() // get client time now to avoid accuracy issues caused by unpredictable sync time
|
||||||
output, err := GetSSHOutput(sshClient, "libmuttonserver fetch", (deviceIDList)[0].Name())
|
output, err := GetSSHOutput(sshClient, "libmuttonserver fetch", (deviceIDList)[0].Name())
|
||||||
@@ -427,24 +432,22 @@ func folderSync(folders []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 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, error) {
|
func RunJob(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()
|
sshClient, offlineMode, sshIsWindows, sshEntryRoot, err := GetSSHClient()
|
||||||
|
if offlineMode {
|
||||||
|
return [3][]string{nil, nil, nil}, nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if manualSync {
|
return [3][]string{nil, nil, nil}, errors.New("unable to connect to SSH client: " + err.Error())
|
||||||
return [3][]string{nil, nil, nil}, errors.New("unable to connect to SSH client: " + err.Error())
|
|
||||||
} else {
|
|
||||||
return [3][]string{nil, nil, nil}, nil // return silently if the sync job was called automatically, as the user may just be in offline mode
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
defer func(sshClient *ssh.Client) {
|
defer func(sshClient *ssh.Client) {
|
||||||
_ = sshClient.Close()
|
_ = sshClient.Close()
|
||||||
}(sshClient)
|
}(sshClient)
|
||||||
|
|
||||||
// fetch remote lists
|
// fetch remote lists
|
||||||
remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime, err := getRemoteDataFromClient(sshClient, manualSync)
|
remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime, err := getRemoteDataFromClient(sshClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return [3][]string{nil, nil, nil}, errors.New("unable to fetch remote data: " + err.Error())
|
return [3][]string{nil, nil, nil}, errors.New("unable to fetch remote data: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-12
@@ -11,15 +11,18 @@ 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) error {
|
func ShearRemoteFromClient(targetLocationIncomplete string) error {
|
||||||
deviceID, isDir, err := 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 {
|
if err != nil {
|
||||||
return errors.New("unable to shear target locally: " + err.Error())
|
return errors.New("unable to shear target locally: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !forceOffline && deviceID != "" { // ensure a device ID exists (online mode)
|
if 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
|
||||||
sshClient, _, _, err := GetSSHClient()
|
sshClient, offlineMode, _, _, err := GetSSHClient()
|
||||||
|
if offlineMode {
|
||||||
|
goto end
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("unable to connect to SSH client: " + err.Error())
|
return errors.New("unable to connect to SSH client: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -42,13 +45,14 @@ func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
end:
|
||||||
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
|
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) error {
|
func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string) error {
|
||||||
err := synccommon.RenameLocal(oldLocationIncomplete, newLocationIncomplete, false) // move the target on the local system
|
err := synccommon.RenameLocal(oldLocationIncomplete, newLocationIncomplete, false) // move the target on the local system
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("unable to rename target locally: " + err.Error())
|
return errors.New("unable to rename target locally: " + err.Error())
|
||||||
@@ -58,9 +62,12 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("unable to generate device ID list: " + err.Error())
|
return errors.New("unable to generate device ID list: " + err.Error())
|
||||||
}
|
}
|
||||||
if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
if 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
|
||||||
sshClient, _, _, err := GetSSHClient()
|
sshClient, offlineMode, _, _, err := GetSSHClient()
|
||||||
|
if offlineMode {
|
||||||
|
goto end
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("unable to connect to SSH client: " + err.Error())
|
return errors.New("unable to connect to SSH client: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -81,13 +88,14 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
end:
|
||||||
back.Exit(0)
|
back.Exit(0)
|
||||||
return nil
|
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) error {
|
func AddFolderRemoteFromClient(targetLocationIncomplete string) error {
|
||||||
err := synccommon.AddFolderLocal(targetLocationIncomplete) // add the folder on the local system
|
err := synccommon.AddFolderLocal(targetLocationIncomplete) // add the folder on the local system
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("unable to add folder locally: " + err.Error())
|
return errors.New("unable to add folder locally: " + err.Error())
|
||||||
@@ -97,9 +105,12 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline boo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("unable to generate device ID list: " + err.Error())
|
return errors.New("unable to generate device ID list: " + err.Error())
|
||||||
}
|
}
|
||||||
if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
if 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
|
||||||
sshClient, _, _, err := GetSSHClient()
|
sshClient, offlineMode, _, _, err := GetSSHClient()
|
||||||
|
if offlineMode {
|
||||||
|
goto end
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("unable to connect to SSH client: " + err.Error())
|
return errors.New("unable to connect to SSH client: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -117,6 +128,7 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
end:
|
||||||
back.Exit(0)
|
back.Exit(0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -37,8 +37,7 @@ func DeviceIDGen(oldDeviceID string) (string, string, 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
|
||||||
// also removes the old device ID file (remotely)
|
// also removes the old device ID file (remotely)
|
||||||
// manualSync is true so the user is alerted if device ID registration fails
|
sshClient, _, _, _, err := syncclient.GetSSHClient()
|
||||||
sshClient, _, _, err := syncclient.GetSSHClient()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", errors.New("unable to connect to SSH client: " + err.Error())
|
return "", "", errors.New("unable to connect to SSH client: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user