Add offline mode option to libmutton.ini

This commit is contained in:
2025-05-29 23:39:15 -04:00
parent 9ffd6d5cef
commit 3dd07e7165
5 changed files with 62 additions and 41 deletions
+6
View File
@@ -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.
// 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).
// If requesting SSH config, request "LIBMUTTON/offlineMode" first to avoid errors.
func ParseConfig(valuesRequested [][2]string) ([]string, error) {
cfg, err := loadConfig()
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])
}
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
+2 -1
View File
@@ -47,6 +47,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
err = cfg.WriteConfig(append(
clientSpecificIniData,
[][3]string{
{"LIBMUTTON", "offlineMode", "false"},
{"LIBMUTTON", "sshUser", sshUser},
{"LIBMUTTON", "sshIP", sshIP},
{"LIBMUTTON", "sshPort", sshPort},
@@ -74,7 +75,7 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][
}
// write config file
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 {
return errors.New("unable to write config file: " + err.Error())
}
+28 -25
View File
@@ -17,13 +17,22 @@ import (
"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).
func GetSSHClient() (*ssh.Client, string, bool, error) {
// get SSH config info, exit if not configured
sshUserConfig, err := cfg.ParseConfig([][2]string{{"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshIsWindows"}})
func GetSSHClient() (*ssh.Client, bool, bool, string, error) {
// get SSH config info
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 {
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
@@ -45,7 +54,7 @@ func GetSSHClient() (*ssh.Client, string, bool, error) {
case 6:
isWindows, err = strconv.ParseBool(key)
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
key, err := os.ReadFile(keyFile)
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
@@ -64,14 +73,14 @@ func GetSSHClient() (*ssh.Client, string, bool, error) {
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, global.GetPassphrase("Enter passphrase for your SSH keyfile:"))
}
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
var hostKeyCallback ssh.HostKeyCallback
hostKeyCallback, err = knownhosts.New(back.Home + global.PathSeparator + ".ssh" + global.PathSeparator + "known_hosts")
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
@@ -87,10 +96,10 @@ func GetSSHClient() (*ssh.Client, string, bool, error) {
// connect to SSH server
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
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.
@@ -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.
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
deviceIDList, err := global.GenDeviceIDList()
if err != nil {
return nil, nil, nil, 0, 0, err
}
if len(deviceIDList) == 0 {
if manualSync {
return nil, nil, nil, 0, 0, errors.New("no device ID found")
} else {
back.Exit(0) // exit silently if the sync job was called automatically, as the user may just be in offline mode
}
}
clientTime := time.Now().Unix() // get client time now to avoid accuracy issues caused by unpredictable sync time
output, err := GetSSHOutput(sshClient, "libmuttonserver fetch", (deviceIDList)[0].Name())
@@ -427,24 +432,22 @@ func folderSync(folders []string) error {
}
// RunJob runs the SSH sync job.
// Setting manualSync to true will throw errors if sync is not configured (online mode is assumed).
// Setting returnLists to true will return the deletions, downloads, and uploads lists for use by the client.
func RunJob(manualSync, returnLists bool) ([3][]string, error) {
func RunJob(returnLists bool) ([3][]string, error) {
// get SSH client to re-use throughout the sync process
sshClient, sshEntryRoot, sshIsWindows, err := GetSSHClient()
if err != nil {
if manualSync {
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
sshClient, offlineMode, sshIsWindows, sshEntryRoot, err := GetSSHClient()
if offlineMode {
return [3][]string{nil, nil, nil}, nil
}
if err != nil {
return [3][]string{nil, nil, nil}, errors.New("unable to connect to SSH client: " + err.Error())
}
defer func(sshClient *ssh.Client) {
_ = sshClient.Close()
}(sshClient)
// fetch remote lists
remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime, err := getRemoteDataFromClient(sshClient, manualSync)
remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime, err := getRemoteDataFromClient(sshClient)
if err != nil {
return [3][]string{nil, nil, nil}, errors.New("unable to fetch remote data: " + err.Error())
}
+24 -12
View File
@@ -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.
// 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
if err != nil {
return errors.New("unable to shear target locally: " + err.Error())
}
if !forceOffline && deviceID != "" { // ensure a device ID exists (online mode)
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
sshClient, _, _, err := GetSSHClient()
if deviceID != "" { // ensure a device ID exists (online mode)
// create an SSH client
sshClient, offlineMode, _, _, err := GetSSHClient()
if offlineMode {
goto end
}
if err != nil {
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
return nil
}
// RenameRemoteFromClient renames oldLocationIncomplete to newLocationIncomplete on the local system and calls the server to perform the rename remotely and add the old target to the deletions list.
// It can safely be called in offline mode, as well, so this is the intended interface for renaming (RenameLocal should only be used directly by the server binary).
func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string, forceOffline bool) error {
func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string) error {
err := synccommon.RenameLocal(oldLocationIncomplete, newLocationIncomplete, false) // move the target on the local system
if err != nil {
return errors.New("unable to rename target locally: " + err.Error())
@@ -58,9 +62,12 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string,
if err != nil {
return errors.New("unable to generate device ID list: " + err.Error())
}
if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
sshClient, _, _, err := GetSSHClient()
if len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
// create an SSH client
sshClient, offlineMode, _, _, err := GetSSHClient()
if offlineMode {
goto end
}
if err != nil {
return errors.New("unable to connect to SSH client: " + err.Error())
}
@@ -81,13 +88,14 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string,
}
}
end:
back.Exit(0)
return nil
}
// AddFolderRemoteFromClient creates a new entry-containing directory on the local system and calls the server to create the folder remotely.
// It can safely be called in offline mode, as well, so this is the intended interface for adding folders (AddFolderLocal should only be used directly by the server binary).
func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline bool) error {
func AddFolderRemoteFromClient(targetLocationIncomplete string) error {
err := synccommon.AddFolderLocal(targetLocationIncomplete) // add the folder on the local system
if err != nil {
return errors.New("unable to add folder locally: " + err.Error())
@@ -97,9 +105,12 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline boo
if err != nil {
return errors.New("unable to generate device ID list: " + err.Error())
}
if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
sshClient, _, _, err := GetSSHClient()
if len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
// create an SSH client
sshClient, offlineMode, _, _, err := GetSSHClient()
if offlineMode {
goto end
}
if err != nil {
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)
return nil
}
+1 -2
View File
@@ -37,8 +37,7 @@ func DeviceIDGen(oldDeviceID string) (string, string, error) {
// register new device ID with server and fetch remote EntryRoot and OS type
// 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 {
return "", "", errors.New("unable to connect to SSH client: " + err.Error())
}