mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-27 20:36:29 -04:00
Initial password aging support
This commit is contained in:
+176
-81
@@ -23,19 +23,20 @@ import (
|
||||
// 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),
|
||||
// sshAgeDir (the directory housing age files on the remote server),
|
||||
// Only supports key-based authentication (passwords are supported for CLI-based implementations).
|
||||
func GetSSHClient() (*ssh.Client, bool, bool, string, error) {
|
||||
func GetSSHClient() (*ssh.Client, bool, bool, string, 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"}})
|
||||
sshUserConfig, err := cfg.ParseConfig([][2]string{{"LIBMUTTON", "offlineMode"}, {"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshAgeDir"}, {"LIBMUTTON", "sshIsWindows"}})
|
||||
if len(sshUserConfig) == 1 {
|
||||
// offline mode is enabled
|
||||
return nil, true, false, "", nil
|
||||
return nil, true, false, "", "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, 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, ageDir string
|
||||
var isWindows bool
|
||||
for i, key := range sshUserConfig {
|
||||
switch i {
|
||||
@@ -52,9 +53,11 @@ func GetSSHClient() (*ssh.Client, bool, bool, string, error) {
|
||||
case 6:
|
||||
entryRoot = key
|
||||
case 7:
|
||||
ageDir = key
|
||||
case 8:
|
||||
isWindows, err = strconv.ParseBool(key)
|
||||
if err != nil {
|
||||
return nil, false, 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +65,7 @@ func GetSSHClient() (*ssh.Client, bool, bool, string, error) {
|
||||
// read private key
|
||||
key, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, false, false, "", errors.New("unable to read private key: " + keyFile)
|
||||
return nil, false, false, "", "", errors.New("unable to read private key: " + keyFile)
|
||||
}
|
||||
|
||||
// parse private key
|
||||
@@ -73,14 +76,14 @@ func GetSSHClient() (*ssh.Client, bool, bool, string, error) {
|
||||
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, global.GetPassword("Enter password for your SSH keyfile:"))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, 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, 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
|
||||
@@ -96,10 +99,10 @@ func GetSSHClient() (*ssh.Client, bool, bool, string, error) {
|
||||
// connect to SSH server
|
||||
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
||||
if err != nil {
|
||||
return nil, false, 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, false, isWindows, entryRoot, nil
|
||||
return sshClient, false, isWindows, entryRoot, ageDir, nil
|
||||
}
|
||||
|
||||
// GetSSHOutput runs a command over SSH and returns the output as a string.
|
||||
@@ -127,48 +130,63 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) (string, error) {
|
||||
return outputString, nil
|
||||
}
|
||||
|
||||
// getRemoteDataFromClient returns a map of remote entries to their modification times, a list of remote folders, a list of queued deletions, and the current server&client times as UNIX timestamps.
|
||||
func getRemoteDataFromClient(sshClient *ssh.Client) (map[string]int64, []string, []string, int64, int64, error) {
|
||||
// getRemoteDataFromClient returns:
|
||||
// a map of remote entries to their modification times,
|
||||
// a list of remote age files to their timestamps,
|
||||
// a list of remote folders, a list of queued deletions,
|
||||
// and the current server&client times as UNIX timestamps.
|
||||
func getRemoteDataFromClient(sshClient *ssh.Client) (map[string]int64, 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
|
||||
return nil, nil, nil, nil, 0, 0, err
|
||||
}
|
||||
if len(deviceIDList) == 0 {
|
||||
return nil, nil, nil, 0, 0, errors.New("no device ID found")
|
||||
return nil, nil, nil, nil, 0, 0, errors.New("no device ID found")
|
||||
}
|
||||
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())
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, errors.New("unable to run remote command: " + err.Error())
|
||||
return nil, nil, nil, nil, 0, 0, errors.New("unable to run remote command: " + err.Error())
|
||||
}
|
||||
|
||||
// split output into slice based on occurrences of FSSpace
|
||||
outputSlice := strings.Split(output, global.FSSpace)
|
||||
|
||||
// parse output/re-form lists
|
||||
if len(outputSlice) != 5 { // ensure information from server is complete
|
||||
return nil, nil, nil, 0, 0, errors.New("unable to run remote command; server returned an unexpected response")
|
||||
if len(outputSlice) != 7 { // ensure information from server is complete
|
||||
return nil, nil, nil, nil, 0, 0, errors.New("unable to run remote command; server returned an unexpected response")
|
||||
}
|
||||
serverTime, err := strconv.ParseInt(outputSlice[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, errors.New("unable to parse server time: " + err.Error())
|
||||
return nil, nil, nil, nil, 0, 0, errors.New("unable to parse server time: " + err.Error())
|
||||
}
|
||||
entries := strings.Split(outputSlice[1], global.FSMisc)[1:]
|
||||
modsStrings := strings.Split(outputSlice[2], global.FSMisc)[1:]
|
||||
folders := strings.Split(outputSlice[3], global.FSMisc)[1:]
|
||||
deletions := strings.Split(outputSlice[4], global.FSMisc)[1:]
|
||||
ageFiles := strings.Split(outputSlice[3], global.FSMisc)[1:]
|
||||
ageFilesTimestampStrings := strings.Split(outputSlice[4], global.FSMisc)[1:]
|
||||
folders := strings.Split(outputSlice[5], global.FSMisc)[1:]
|
||||
deletions := strings.Split(outputSlice[6], global.FSMisc)[1:]
|
||||
|
||||
// convert the mod times to int64
|
||||
// convert the mod times+age timestamps to int64
|
||||
var mods []int64
|
||||
var mod int64
|
||||
for _, modString := range modsStrings {
|
||||
mod, err = strconv.ParseInt(modString, 10, 64)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, errors.New("unable to parse mod time: " + err.Error())
|
||||
return nil, nil, nil, nil, 0, 0, errors.New("unable to parse mod time: " + err.Error())
|
||||
}
|
||||
mods = append(mods, mod)
|
||||
}
|
||||
var timestamps []int64
|
||||
var timestamp int64
|
||||
for _, ageFilesTimestampString := range ageFilesTimestampStrings {
|
||||
timestamp, err = strconv.ParseInt(ageFilesTimestampString, 10, 64)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, 0, 0, errors.New("unable to parse age timestamp: " + err.Error())
|
||||
}
|
||||
timestamps = append(timestamps, timestamp)
|
||||
}
|
||||
|
||||
// map remote entries to their modification times
|
||||
entryModMap := make(map[string]int64)
|
||||
@@ -176,7 +194,13 @@ func getRemoteDataFromClient(sshClient *ssh.Client) (map[string]int64, []string,
|
||||
entryModMap[entry] = mods[i]
|
||||
}
|
||||
|
||||
return entryModMap, folders, deletions, serverTime, clientTime, nil
|
||||
// map remote age files to their timestamps
|
||||
ageTimestampMap := make(map[string]int64)
|
||||
for i, ageFile := range ageFiles {
|
||||
ageTimestampMap[ageFile] = timestamps[i]
|
||||
}
|
||||
|
||||
return entryModMap, ageTimestampMap, folders, deletions, serverTime, clientTime, nil
|
||||
}
|
||||
|
||||
// getLocalData returns a map of local entries to their modification times.
|
||||
@@ -200,17 +224,26 @@ func getLocalData() (map[string]int64, error) {
|
||||
return entryModMap, nil
|
||||
}
|
||||
|
||||
// targetLocationFormatSFTP formats the target location to match the remote server's entry directory and path separator.
|
||||
func targetLocationFormatSFTP(targetName, serverEntryRoot string, serverIsWindows bool) string {
|
||||
// getRealPathSFTP formats the vanityPath to match the remote server's entry/age file directory and path separator.
|
||||
func getRealPathSFTP(vanityPath, serverEntryRoot string, serverIsWindows bool) string {
|
||||
if !serverIsWindows {
|
||||
return serverEntryRoot + targetName
|
||||
return serverEntryRoot + vanityPath
|
||||
} else {
|
||||
return serverEntryRoot + strings.ReplaceAll(targetName, "/", "\\")
|
||||
return serverEntryRoot + strings.ReplaceAll(vanityPath, "/", "\\")
|
||||
}
|
||||
}
|
||||
|
||||
// getRealPathSFTP formats the vanityPath to match the remote server's entry/age file directory and path separator.
|
||||
func getRealAgePathSFTP(vanityPath, serverAgeDir string, serverIsWindows bool) string {
|
||||
if !serverIsWindows {
|
||||
return serverAgeDir + "/" + strings.ReplaceAll(vanityPath, "/", global.FSPath)
|
||||
} else {
|
||||
return serverAgeDir + "\\" + strings.ReplaceAll(vanityPath, "/", global.FSPath)
|
||||
}
|
||||
}
|
||||
|
||||
// sftpSync takes two slices of entries (one for downloads and one for uploads) and syncs them between the client and server using SFTP.
|
||||
func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, downloadList, uploadList []string) error {
|
||||
func sftpSync(sshClient *ssh.Client, sshEntryRoot, sshAgeDir string, sshIsWindows bool, downloadList, uploadList []string) error {
|
||||
// create an SFTP client from sshClient
|
||||
sftpClient, err := sftp.NewClient(sshClient)
|
||||
if err != nil {
|
||||
@@ -222,17 +255,28 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
|
||||
// iterate over the download list
|
||||
var filesTransferred bool
|
||||
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 " + back.AnsiGreen + entryName + back.AnsiReset)
|
||||
for _, vanityPath := range downloadList {
|
||||
// determine if remote file is an age file
|
||||
var isAgeFile bool
|
||||
if strings.HasPrefix(vanityPath, global.FSMisc) {
|
||||
vanityPath = strings.TrimLeft(vanityPath, global.FSMisc)
|
||||
isAgeFile = true
|
||||
} else {
|
||||
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 " + back.AnsiGreen + vanityPath + back.AnsiReset)
|
||||
}
|
||||
|
||||
// store path to remote entry
|
||||
remoteEntryFullPath := targetLocationFormatSFTP(entryName, sshEntryRoot, sshIsWindows)
|
||||
var remoteFileRealPath string
|
||||
if isAgeFile {
|
||||
remoteFileRealPath = getRealAgePathSFTP(vanityPath, sshAgeDir, sshIsWindows)
|
||||
} else {
|
||||
remoteFileRealPath = getRealPathSFTP(vanityPath, sshEntryRoot, sshIsWindows)
|
||||
}
|
||||
|
||||
// save modification time of remote file
|
||||
var fileInfo os.FileInfo
|
||||
fileInfo, err = sftpClient.Stat(remoteEntryFullPath)
|
||||
fileInfo, err = sftpClient.Stat(remoteFileRealPath)
|
||||
if err != nil {
|
||||
return errors.New("unable to get remote file info (mod time): " + err.Error())
|
||||
}
|
||||
@@ -240,17 +284,22 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
|
||||
// open remote file
|
||||
var remoteFile *sftp.File
|
||||
remoteFile, err = sftpClient.Open(remoteEntryFullPath)
|
||||
remoteFile, err = sftpClient.Open(remoteFileRealPath)
|
||||
if err != nil {
|
||||
return errors.New("unable to open remote file: " + err.Error())
|
||||
}
|
||||
|
||||
// store path to local entry
|
||||
localEntryFullPath := global.TargetLocationFormat(entryName)
|
||||
// store path to local file
|
||||
var localFileRealPath string
|
||||
if isAgeFile {
|
||||
localFileRealPath = global.GetRealAgePath(vanityPath)
|
||||
} else {
|
||||
localFileRealPath = global.GetRealPath(vanityPath)
|
||||
}
|
||||
|
||||
// create local file
|
||||
var localFile *os.File
|
||||
localFile, err = os.OpenFile(localEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||
localFile, err = os.OpenFile(localFileRealPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return errors.New("unable to create local file: " + err.Error())
|
||||
}
|
||||
@@ -266,7 +315,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
_ = localFile.Close()
|
||||
|
||||
// set the modification time of the local file to match the value saved from the remote file (from before the download)
|
||||
err = os.Chtimes(localEntryFullPath, time.Now(), modTime)
|
||||
err = os.Chtimes(localFileRealPath, time.Now(), modTime)
|
||||
if err != nil {
|
||||
return errors.New("unable to set local file modification time: " + err.Error())
|
||||
}
|
||||
@@ -278,17 +327,28 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
|
||||
// iterate over the upload list
|
||||
filesTransferred = false
|
||||
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)
|
||||
for _, vanityPath := range uploadList {
|
||||
// determine if local file is an age file
|
||||
var isAgeFile bool
|
||||
if strings.HasPrefix(vanityPath, global.FSMisc) {
|
||||
vanityPath = strings.TrimLeft(vanityPath, global.FSMisc)
|
||||
isAgeFile = true
|
||||
} else {
|
||||
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 " + back.AnsiBlue + vanityPath + back.AnsiReset)
|
||||
}
|
||||
|
||||
fmt.Println("Uploading " + back.AnsiBlue + entryName + back.AnsiReset)
|
||||
|
||||
// store path to local entry
|
||||
localEntryFullPath := global.TargetLocationFormat(entryName)
|
||||
// store path to local file
|
||||
var localFileRealPath string
|
||||
if isAgeFile {
|
||||
localFileRealPath = global.GetRealAgePath(vanityPath)
|
||||
} else {
|
||||
localFileRealPath = global.GetRealPath(vanityPath)
|
||||
}
|
||||
|
||||
// save modification time of local file
|
||||
var fileInfo os.FileInfo
|
||||
fileInfo, err = os.Stat(localEntryFullPath)
|
||||
fileInfo, err = os.Stat(localFileRealPath)
|
||||
if err != nil {
|
||||
return errors.New("unable to get local file info (mod time): " + err.Error())
|
||||
}
|
||||
@@ -296,17 +356,22 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
|
||||
// open local file
|
||||
var localFile *os.File
|
||||
localFile, err = os.Open(localEntryFullPath)
|
||||
localFile, err = os.Open(localFileRealPath)
|
||||
if err != nil {
|
||||
return errors.New("unable to open local file: " + err.Error())
|
||||
}
|
||||
|
||||
// store path to remote entry
|
||||
remoteEntryFullPath := targetLocationFormatSFTP(entryName, sshEntryRoot, sshIsWindows)
|
||||
var remoteFileRealPath string
|
||||
if isAgeFile {
|
||||
remoteFileRealPath = getRealAgePathSFTP(vanityPath, sshAgeDir, sshIsWindows)
|
||||
} else {
|
||||
remoteFileRealPath = getRealPathSFTP(vanityPath, sshEntryRoot, sshIsWindows)
|
||||
}
|
||||
|
||||
// create remote file
|
||||
var remoteFile *sftp.File
|
||||
remoteFile, err = sftpClient.OpenFile(remoteEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY)
|
||||
remoteFile, err = sftpClient.OpenFile(remoteFileRealPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY)
|
||||
if err != nil {
|
||||
return errors.New("unable to create remote file: " + err.Error())
|
||||
}
|
||||
@@ -322,13 +387,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
_ = remoteFile.Close()
|
||||
|
||||
// set permissions on remote file
|
||||
err = sftpClient.Chmod(remoteEntryFullPath, 0600)
|
||||
err = sftpClient.Chmod(remoteFileRealPath, 0600)
|
||||
if err != nil {
|
||||
return errors.New("unable to set permissions on remote file: " + err.Error())
|
||||
}
|
||||
|
||||
// set the modification time of the remote file to match the value saved from the local file (from before the upload)
|
||||
err = sftpClient.Chtimes(remoteEntryFullPath, time.Now(), modTime)
|
||||
err = sftpClient.Chtimes(remoteFileRealPath, time.Now(), modTime)
|
||||
if err != nil {
|
||||
return errors.New("unable to set remote file modification time: " + err.Error())
|
||||
}
|
||||
@@ -343,40 +408,59 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
|
||||
// syncLists determines which entries need to be downloaded and uploaded for synchronizations and calls sftpSync with this information.
|
||||
// Using maps means that syncing will be done in an arbitrary order, but it is a worthy tradeoff for speed and simplicity.
|
||||
func syncLists(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows, timeSynced, returnLists bool, localEntryModMap, remoteEntryModMap map[string]int64) ([3][]string, error) {
|
||||
func syncLists(sshClient *ssh.Client, sshEntryRoot, sshAgeDir string, sshIsWindows, timeSynced, returnLists bool, localEntryModMap, remoteEntryModMap, localAgeTimestampMap, remoteAgeTimestampMap map[string]int64) ([3][]string, error) {
|
||||
// initialize slices to store entries that need to be downloaded or uploaded
|
||||
var downloadList, uploadList []string
|
||||
|
||||
// iterate over client entries
|
||||
for entry, localModTime := range localEntryModMap {
|
||||
// check if the entry is present in the server map
|
||||
if remoteModTime, present := remoteEntryModMap[entry]; present {
|
||||
// entry exists on both client and server, compare mod times
|
||||
if remoteModTime > localModTime {
|
||||
fmt.Println(back.AnsiGreen+entry+back.AnsiReset, "is newer on server, adding to download list")
|
||||
downloadList = append(downloadList, entry)
|
||||
} else if remoteModTime < localModTime {
|
||||
fmt.Println(back.AnsiBlue+entry+back.AnsiReset, "is newer on client, adding to upload list")
|
||||
uploadList = append(uploadList, entry)
|
||||
localMapIter := func(localMap, remoteMap map[string]int64, forAging bool) {
|
||||
for file, localTime := range localMap {
|
||||
// check if the entry is present in the server map
|
||||
if remoteTime, present := remoteMap[file]; present {
|
||||
// entry exists on both client and server, compare mod times
|
||||
if remoteTime > localTime {
|
||||
if !forAging {
|
||||
fmt.Println(back.AnsiGreen+file+back.AnsiReset, "is newer on server, adding to download list")
|
||||
downloadList = append(downloadList, file)
|
||||
} else {
|
||||
downloadList = append(downloadList, global.FSMisc+file)
|
||||
}
|
||||
} else if remoteTime < localTime {
|
||||
if !forAging {
|
||||
fmt.Println(back.AnsiBlue+file+back.AnsiReset, "is newer on client, adding to upload list")
|
||||
uploadList = append(uploadList, file)
|
||||
} else {
|
||||
uploadList = append(uploadList, global.FSMisc+file)
|
||||
}
|
||||
}
|
||||
// remove entry from remoteMap (process of elimination)
|
||||
delete(remoteMap, file)
|
||||
} else {
|
||||
if !forAging {
|
||||
fmt.Println(back.AnsiBlue+file+back.AnsiReset, "does not exist on server, adding to upload list")
|
||||
uploadList = append(uploadList, file)
|
||||
} else {
|
||||
uploadList = append(uploadList, global.FSMisc+file)
|
||||
}
|
||||
}
|
||||
// remove entry from remoteEntryModMap (process of elimination)
|
||||
delete(remoteEntryModMap, entry)
|
||||
} else {
|
||||
fmt.Println(back.AnsiBlue+entry+back.AnsiReset, "does not exist on server, adding to upload list")
|
||||
uploadList = append(uploadList, entry)
|
||||
}
|
||||
}
|
||||
localMapIter(localEntryModMap, remoteEntryModMap, false)
|
||||
localMapIter(localAgeTimestampMap, remoteAgeTimestampMap, true)
|
||||
|
||||
// iterate over remaining entries in remoteEntryModMap
|
||||
// iterate over remaining entries in remote maps
|
||||
for entry := range remoteEntryModMap {
|
||||
fmt.Println(back.AnsiGreen+entry+back.AnsiReset, "does not exist on client, adding to download list")
|
||||
downloadList = append(downloadList, entry)
|
||||
}
|
||||
for ageFile := range remoteAgeTimestampMap {
|
||||
downloadList = append(downloadList, global.FSMisc+ageFile)
|
||||
}
|
||||
|
||||
// call sftpSync with the download and upload lists
|
||||
if timeSynced && (max(len(downloadList), len(uploadList)) > 0) { // only call sftpSync if there are entries to download or upload
|
||||
fmt.Println() // add a gap between list-add messages and the actual sync messages from sftpSync
|
||||
err := sftpSync(sshClient, sshEntryRoot, sshIsWindows, downloadList, uploadList)
|
||||
err := sftpSync(sshClient, sshEntryRoot, sshAgeDir, sshIsWindows, downloadList, uploadList)
|
||||
if err != nil {
|
||||
return [3][]string{nil, nil, nil}, errors.New("unable to sync entries: " + err.Error())
|
||||
}
|
||||
@@ -395,16 +479,22 @@ func syncLists(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows, timeSyn
|
||||
|
||||
// deletionSync removes entries from the client that have been deleted on the server (multi-client deletion).
|
||||
func deletionSync(deletions []string) error {
|
||||
var filesDeleted bool
|
||||
var entryDeleted bool
|
||||
for _, deletion := range deletions {
|
||||
filesDeleted = true // set a flag to indicate that files have been deleted (used to determine whether to print a gap between deletion and other messages)
|
||||
fmt.Println(synccommon.AnsiDelete+deletion+back.AnsiReset, "has been sheared, removing locally (if it exists)")
|
||||
err := os.RemoveAll(global.TargetLocationFormat(deletion))
|
||||
deletionSplit := strings.Split(deletion, global.FSSpace)
|
||||
if deletionSplit[0] == "entry" {
|
||||
entryDeleted = true // set a flag to indicate that at least one entry has been deleted (used to determine whether to print a gap between deletion and other messages)
|
||||
fmt.Println(synccommon.AnsiDelete+deletionSplit[1]+back.AnsiReset, "has been sheared, removing locally (if it exists)")
|
||||
}
|
||||
err := os.RemoveAll(global.GetRealPath(deletionSplit[1]))
|
||||
if err != nil {
|
||||
return errors.New("unable to shear " + deletion + " locally: " + err.Error())
|
||||
if deletionSplit[0] == "entry" {
|
||||
return errors.New("unable to shear " + deletionSplit[1] + " locally: " + err.Error())
|
||||
}
|
||||
return errors.New("unable to shear age file for " + deletionSplit[1] + " locally: " + err.Error())
|
||||
}
|
||||
}
|
||||
if filesDeleted {
|
||||
if entryDeleted {
|
||||
fmt.Println() // add a gap between deletion and other messages
|
||||
}
|
||||
return nil
|
||||
@@ -414,7 +504,7 @@ func deletionSync(deletions []string) error {
|
||||
func folderSync(folders []string) error {
|
||||
for _, folder := range folders {
|
||||
// store the full local path of the folder
|
||||
folderFullPath := global.TargetLocationFormat(folder)
|
||||
folderFullPath := global.GetRealPath(folder)
|
||||
|
||||
// check if target path already exists
|
||||
isAccessible, err := back.TargetIsFile(folderFullPath, false)
|
||||
@@ -435,7 +525,7 @@ func folderSync(folders []string) error {
|
||||
// Setting returnLists to true will return the deletions, downloads, and uploads lists for use by the client.
|
||||
func RunJob(returnLists bool) ([3][]string, error) {
|
||||
// get SSH client to re-use throughout the sync process
|
||||
sshClient, offlineMode, sshIsWindows, sshEntryRoot, err := GetSSHClient()
|
||||
sshClient, offlineMode, sshIsWindows, sshEntryRoot, sshAgeDir, err := GetSSHClient()
|
||||
if offlineMode {
|
||||
return [3][]string{nil, nil, nil}, nil
|
||||
}
|
||||
@@ -447,7 +537,7 @@ func RunJob(returnLists bool) ([3][]string, error) {
|
||||
}(sshClient)
|
||||
|
||||
// fetch remote lists
|
||||
remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime, err := getRemoteDataFromClient(sshClient)
|
||||
remoteEntryModMap, remoteAgeTimestampMap, 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())
|
||||
}
|
||||
@@ -469,6 +559,11 @@ func RunJob(returnLists bool) ([3][]string, error) {
|
||||
if err != nil {
|
||||
return [3][]string{nil, nil, nil}, errors.New("unable to fetch local entry data: " + err.Error())
|
||||
}
|
||||
var localAgeTimestampMap map[string]int64
|
||||
localAgeTimestampMap, err = synccommon.GetEntryAges()
|
||||
if err != nil {
|
||||
return [3][]string{nil, nil, nil}, err
|
||||
}
|
||||
|
||||
// before syncing lists, ensure the client and server clocks are synced within 45 seconds
|
||||
var timeSynced = true
|
||||
@@ -481,14 +576,14 @@ func RunJob(returnLists bool) ([3][]string, error) {
|
||||
// sync new and updated entries
|
||||
var lists [3][]string
|
||||
if returnLists {
|
||||
lists, err = syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap)
|
||||
lists, err = syncLists(sshClient, sshEntryRoot, sshAgeDir, sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap, localAgeTimestampMap, remoteAgeTimestampMap)
|
||||
if err != nil {
|
||||
return [3][]string{nil, nil, nil}, errors.New("unable to sync entries: " + err.Error())
|
||||
}
|
||||
lists[0] = deletions
|
||||
return lists, nil
|
||||
}
|
||||
_, err = syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, false, localEntryModMap, remoteEntryModMap)
|
||||
_, err = syncLists(sshClient, sshEntryRoot, sshAgeDir, sshIsWindows, timeSynced, false, localEntryModMap, remoteEntryModMap, localAgeTimestampMap, remoteAgeTimestampMap)
|
||||
if err != nil {
|
||||
return [3][]string{nil, nil, nil}, errors.New("unable to sync entries: " + err.Error())
|
||||
}
|
||||
|
||||
+25
-20
@@ -15,13 +15,14 @@ import (
|
||||
// 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) error {
|
||||
deviceID, isDir, err := synccommon.ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client
|
||||
func ShearRemoteFromClient(vanityPath string, onlyShearAgingFile bool) error {
|
||||
deviceID, isDir, err := synccommon.ShearLocal(vanityPath, "", onlyShearAgingFile) // 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())
|
||||
}
|
||||
|
||||
sshClient, offlineMode, _, _, err := GetSSHClient()
|
||||
var modifier string
|
||||
sshClient, offlineMode, _, _, _, err := GetSSHClient()
|
||||
if offlineMode {
|
||||
goto end
|
||||
}
|
||||
@@ -32,13 +33,16 @@ func ShearRemoteFromClient(targetLocationIncomplete string) error {
|
||||
return errors.New("unable to shear target remotely: no device ID found")
|
||||
}
|
||||
|
||||
// ensure targetLocationIncomplete ends with a slash if it is a directory (for clarity in shear message)
|
||||
if isDir && !strings.HasSuffix(targetLocationIncomplete, "/") {
|
||||
targetLocationIncomplete += "/"
|
||||
// ensure vanityPath ends with a slash if it is a directory (for clarity in shear message)
|
||||
if isDir && !strings.HasSuffix(vanityPath, "/") {
|
||||
vanityPath += "/"
|
||||
}
|
||||
|
||||
// call the server to remotely shear the target and add it to the deletions list
|
||||
_, err = GetSSHOutput(sshClient, "libmuttonserver shear", deviceID+"\n"+strings.ReplaceAll(targetLocationIncomplete, global.PathSeparator, global.FSPath))
|
||||
if onlyShearAgingFile {
|
||||
modifier = "-age"
|
||||
}
|
||||
_, err = GetSSHOutput(sshClient, "libmuttonserver shear"+modifier, deviceID+"\n"+strings.ReplaceAll(vanityPath, global.PathSeparator, global.FSPath))
|
||||
if err != nil {
|
||||
return errors.New("unable to shear target remotely: " + err.Error())
|
||||
}
|
||||
@@ -54,13 +58,13 @@ end:
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenameRemoteFromClient renames oldLocationIncomplete to newLocationIncomplete on
|
||||
// RenameRemoteFromClient renames oldVanityPath to newVanityPath 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) error {
|
||||
err := synccommon.RenameLocal(oldLocationIncomplete, newLocationIncomplete) // move the target on the local system
|
||||
func RenameRemoteFromClient(oldVanityPath, newVanityPath string) error {
|
||||
err := synccommon.RenameLocal(oldVanityPath, newVanityPath) // move the target on the local system
|
||||
if err != nil {
|
||||
return errors.New("unable to rename target locally: " + err.Error())
|
||||
}
|
||||
@@ -69,23 +73,24 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string)
|
||||
if err != nil {
|
||||
return errors.New("unable to generate device ID list: " + err.Error())
|
||||
}
|
||||
if deviceIDList[0].Name() == "" {
|
||||
return errors.New("unable to rename target remotely: no device ID found")
|
||||
}
|
||||
|
||||
// create an SSH client
|
||||
sshClient, offlineMode, _, _, err := GetSSHClient()
|
||||
sshClient, offlineMode, _, _, _, err := GetSSHClient()
|
||||
if offlineMode {
|
||||
goto end
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New("unable to connect to SSH client: " + err.Error())
|
||||
}
|
||||
if deviceIDList[0].Name() == "" {
|
||||
return errors.New("unable to rename target remotely: no device ID found")
|
||||
}
|
||||
|
||||
// call the server to move the target on the remote system and add the old target to the deletions list
|
||||
_, err = GetSSHOutput(sshClient, "libmuttonserver rename",
|
||||
(deviceIDList)[0].Name()+"\n"+
|
||||
strings.ReplaceAll(oldLocationIncomplete, global.PathSeparator, global.FSPath)+"\n"+
|
||||
strings.ReplaceAll(newLocationIncomplete, global.PathSeparator, global.FSPath))
|
||||
strings.ReplaceAll(oldVanityPath, global.PathSeparator, global.FSPath)+"\n"+
|
||||
strings.ReplaceAll(newVanityPath, global.PathSeparator, global.FSPath))
|
||||
if err != nil {
|
||||
return errors.New("unable to rename target remotely: " + err.Error())
|
||||
}
|
||||
@@ -106,14 +111,14 @@ end:
|
||||
// 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) error {
|
||||
err := synccommon.AddFolderLocal(targetLocationIncomplete) // add the folder on the local system
|
||||
func AddFolderRemoteFromClient(vanityPath string) error {
|
||||
err := synccommon.AddFolderLocal(vanityPath) // add the folder on the local system
|
||||
if err != nil {
|
||||
return errors.New("unable to add folder locally: " + err.Error())
|
||||
}
|
||||
|
||||
// create an SSH client
|
||||
sshClient, offlineMode, _, _, err := GetSSHClient()
|
||||
sshClient, offlineMode, _, _, _, err := GetSSHClient()
|
||||
if offlineMode {
|
||||
goto end
|
||||
}
|
||||
@@ -122,7 +127,7 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string) error {
|
||||
}
|
||||
|
||||
// call the server to create the folder remotely
|
||||
_, err = GetSSHOutput(sshClient, "libmuttonserver addfolder", strings.ReplaceAll(targetLocationIncomplete, global.PathSeparator, global.FSPath)) // call the server to create the folder remotely
|
||||
_, err = GetSSHOutput(sshClient, "libmuttonserver addfolder", strings.ReplaceAll(vanityPath, global.PathSeparator, global.FSPath)) // call the server to create the folder remotely
|
||||
if err != nil {
|
||||
return errors.New("unable to add folder remotely: " + err.Error())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user