From 5d4232637b17a8455b8c2525ec42661cb8236c0a Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Thu, 30 May 2024 22:12:29 -0400 Subject: [PATCH] Lay groundwork for NT/UNIX-like sync interoperability --- src/backend/targetLocationFormatUNIX.go | 6 +-- src/backend/targetLocationFormatWIN.go | 6 +-- src/sync/2globalsUNIX.go | 5 --- src/sync/2globalsWIN.go | 5 --- src/sync/client.go | 51 ++++++++++++++++++------- src/sync/common.go | 8 ++-- src/sync/server.go | 27 ++----------- src/sync/serverPrintToStdoutUNIX.go | 12 ++++++ src/sync/serverPrintToStdoutWIN.go | 13 +++++++ 9 files changed, 77 insertions(+), 56 deletions(-) delete mode 100644 src/sync/2globalsUNIX.go delete mode 100644 src/sync/2globalsWIN.go create mode 100644 src/sync/serverPrintToStdoutUNIX.go create mode 100644 src/sync/serverPrintToStdoutWIN.go diff --git a/src/backend/targetLocationFormatUNIX.go b/src/backend/targetLocationFormatUNIX.go index 5e6dee7..7ea85d6 100644 --- a/src/backend/targetLocationFormatUNIX.go +++ b/src/backend/targetLocationFormatUNIX.go @@ -2,7 +2,7 @@ package backend -// TargetLocationFormat returns the target location of an entry formatted for the current platform -func TargetLocationFormat(entryName string) string { - return EntryRoot + PathSeparator + entryName +// TargetLocationFormat returns the full location of an entry (given the name) formatted for the current platform +func TargetLocationFormat(targetLocationIncomplete string) string { + return EntryRoot + targetLocationIncomplete } diff --git a/src/backend/targetLocationFormatWIN.go b/src/backend/targetLocationFormatWIN.go index adbcb84..0a82dfa 100644 --- a/src/backend/targetLocationFormatWIN.go +++ b/src/backend/targetLocationFormatWIN.go @@ -4,7 +4,7 @@ package backend import "strings" -// TargetLocationFormat returns the target location of an entry formatted for the current platform -func TargetLocationFormat(entryName string) string { - return EntryRoot + PathSeparator + strings.ReplaceAll(entryName, "/", PathSeparator) +// TargetLocationFormat returns the full location of an entry (given the name) formatted for the current platform +func TargetLocationFormat(targetLocationIncomplete string) string { + return EntryRoot + strings.ReplaceAll(targetLocationIncomplete, "/", PathSeparator) } diff --git a/src/sync/2globalsUNIX.go b/src/sync/2globalsUNIX.go deleted file mode 100644 index 54758b1..0000000 --- a/src/sync/2globalsUNIX.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !windows - -package sync - -const bareEntryRoot = "/.local/share/libmutton" diff --git a/src/sync/2globalsWIN.go b/src/sync/2globalsWIN.go deleted file mode 100644 index 011c7bf..0000000 --- a/src/sync/2globalsWIN.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build windows - -package sync - -const bareEntryRoot = "\\AppData\\Local\\libmutton\\entries" diff --git a/src/sync/client.go b/src/sync/client.go index 11a2736..ec2138c 100644 --- a/src/sync/client.go +++ b/src/sync/client.go @@ -92,6 +92,7 @@ func getSSHClient(manualSync bool) (*ssh.Client, string) { } // GetSSHOutput runs a command over SSH and returns the output as a string +// TODO run getSSHClient() in RunJob and pass to each function that needs it (to avoid multiple connections), move defer to RunJob func GetSSHOutput(cmd string, manualSync bool) string { sshClient, _ := getSSHClient(manualSync) defer sshClient.Close() @@ -178,6 +179,15 @@ func getLocalData() map[string]int64 { return entryModMap } +// targetLocationFormatSFTP formats the target location to match the remote server's entry directory and path separator +func targetLocationFormatSFTP(targetName, serverEntryRoot string, serverIsWindows bool) string { + if !serverIsWindows { + return serverEntryRoot + targetName + } else { + return serverEntryRoot + strings.ReplaceAll(targetName, "/", "\\") + } +} + // 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(downloadList, uploadList []string, manualSync bool) { // establish an SSH connection for transfers @@ -199,9 +209,12 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { fmt.Println("Downloading " + ansiDownload + entryName + backend.AnsiReset) + // store path to remote entry + remoteEntryFullPath := targetLocationFormatSFTP(entryName, "/home/"+sshUser+"/.local/share/libmutton", false) // TODO temporarily hard-coded to expect a default home folder location on a UNIX-like server + // save modification time of remote file var fileInfo os.FileInfo - fileInfo, err = sftpClient.Stat("/home/" + sshUser + bareEntryRoot + entryName) // TODO does not work if server is hosted on Windows + fileInfo, err = sftpClient.Stat(remoteEntryFullPath) if err != nil { fmt.Println(backend.AnsiError+"Sync failed - Unable to get remote file info (modtime):", err.Error()+backend.AnsiReset) os.Exit(1) @@ -210,15 +223,18 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { // open remote file var remoteFile *sftp.File - remoteFile, err = sftpClient.Open("/home/" + sshUser + bareEntryRoot + entryName) // TODO does not work if server is hosted on Windows + remoteFile, err = sftpClient.Open(remoteEntryFullPath) if err != nil { fmt.Println(backend.AnsiError+"Sync failed - Unable to open remote file:", err.Error()+backend.AnsiReset) os.Exit(1) } + // store path to local entry + localEntryFullPath := backend.TargetLocationFormat(entryName) + // create local file var localFile *os.File - localFile, err = os.Create(backend.EntryRoot + entryName) + localFile, err = os.Create(localEntryFullPath) if err != nil { fmt.Println(backend.AnsiError+"Sync failed - Unable to create local file:", err.Error()+backend.AnsiReset) os.Exit(1) @@ -236,7 +252,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { 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(backend.EntryRoot+entryName, time.Now(), modTime) + err = os.Chtimes(remoteEntryFullPath, time.Now(), modTime) } if filesTransfered { @@ -250,9 +266,12 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { fmt.Println("Uploading " + ansiUpload + entryName + backend.AnsiReset) + // store path to local entry + localEntryFullPath := backend.TargetLocationFormat(entryName) + // save modification time of local file var fileInfo os.FileInfo - fileInfo, err = os.Stat(backend.EntryRoot + entryName) + fileInfo, err = os.Stat(localEntryFullPath) if err != nil { fmt.Println(backend.AnsiError+"Sync failed - Unable to get local file info (modtime):", err.Error()+backend.AnsiReset) os.Exit(1) @@ -261,15 +280,18 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { // open local file var localFile *os.File - localFile, err = os.Open(backend.EntryRoot + entryName) + localFile, err = os.Open(localEntryFullPath) if err != nil { fmt.Println(backend.AnsiError+"Sync failed - Unable to open local file:", err.Error()+backend.AnsiReset) os.Exit(1) } + // store path to remote entry + remoteEntryFullPath := targetLocationFormatSFTP(entryName, "/home/"+sshUser+"/.local/share/libmutton", false) // TODO temporarily hard-coded to expect a default home folder location on a UNIX-like server + // create remote file var remoteFile *sftp.File - remoteFile, err = sftpClient.Create("/home/" + sshUser + bareEntryRoot + entryName) // TODO does not work if server is hosted on Windows + remoteFile, err = sftpClient.Create(remoteEntryFullPath) if err != nil { fmt.Println(backend.AnsiError+"Sync failed - Unable to create remote file:", err.Error()+backend.AnsiReset) os.Exit(1) @@ -287,7 +309,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { remoteFile.Close() // set the modification time of the remote file to match the value saved from the local file (from before the upload) - err = sftpClient.Chtimes("/home/"+sshUser+bareEntryRoot+entryName, time.Now(), modTime) // TODO does not work if server is hosted on Windows + err = sftpClient.Chtimes(remoteEntryFullPath, time.Now(), modTime) } if filesTransfered { @@ -343,7 +365,7 @@ func ShearRemoteFromClient(targetLocationIncomplete string) { // call the server to remotely shear the target and add it to the deletions list // deviceID and targetLocationIncomplete are separated by \x1d, path separators are replaced with \x1e, and spaces are replaced with \x1f TODO is there a need to combine deviceID and targetLocationIncomplete into one argument? - GetSSHOutput("libmuttonserver shear "+deviceID+"\x1d"+strings.ReplaceAll(strings.ReplaceAll(targetLocationIncomplete, backend.PathSeparator, "\x1e"), " ", "\x1f"), false) + GetSSHOutput("libmuttonserver shear "+deviceID+"\x1d"+strings.ReplaceAll(strings.ReplaceAll(targetLocationIncomplete, backend.PathSeparator, "\x1e"), " ", "\x1f"), false) // TODO seems to already have Windows server support, perhaps copy this approach to AddFolderRemoteFromClient os.Exit(0) // sync is not required after shearing since the target has already been removed from the local system } @@ -354,7 +376,7 @@ func deletionSync(deletions []string) { 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+backend.AnsiReset, "has been sheared, removing locally (if it exists)") - os.RemoveAll(backend.EntryRoot + deletion) + os.RemoveAll(backend.TargetLocationFormat(deletion)) } if filesDeleted { @@ -365,7 +387,7 @@ func deletionSync(deletions []string) { // AddFolderRemoteFromClient creates a new entry-containing directory on the local system and calls the server to create the folder remotely func AddFolderRemoteFromClient(targetLocationIncomplete string) { AddFolderLocal(targetLocationIncomplete) // add the folder on the local system - GetSSHOutput("libmuttonserver addfolder "+strings.ReplaceAll(targetLocationIncomplete, " ", "\x1f"), false) // call the server to create the folder remotely + GetSSHOutput("libmuttonserver addfolder "+strings.ReplaceAll(targetLocationIncomplete, " ", "\x1f"), false) // call the server to create the folder remotely TODO Windows server support os.Exit(0) } @@ -373,11 +395,14 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string) { // folderSync creates folders on the client (from the given list of folder names) func folderSync(folders []string) { for _, folder := range folders { + // store the full local path of the folder + folderFullPath := backend.TargetLocationFormat(folder) + // check if folder already exists - isFile, isAccessible := backend.TargetIsFile(backend.EntryRoot+folder, false, 1) + isFile, isAccessible := backend.TargetIsFile(folderFullPath, false, 1) if !isFile && !isAccessible { - os.MkdirAll(backend.EntryRoot+folder, 0700) + os.MkdirAll(folderFullPath, 0700) } else if isFile { fmt.Println(backend.AnsiError + "Sync failed - Failed to create folder \"" + folder + "\" - a file with the same name already exists" + backend.AnsiReset) os.Exit(1) diff --git a/src/sync/common.go b/src/sync/common.go index 39e6644..022aacb 100644 --- a/src/sync/common.go +++ b/src/sync/common.go @@ -10,7 +10,6 @@ import ( ) // WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists) -// initCommand is used to specify to the end user how to generate the entry directory if it does not exists func WalkEntryDir() ([]string, []string) { // define file/directory containing slices so that they may be accessed by the anonymous WalkDir function var fileList []string @@ -51,7 +50,7 @@ func getModTimes(entryList []string) []int64 { // get a list of all entry modification times var modList []int64 for _, file := range entryList { - modTime, _ := os.Stat(backend.EntryRoot + file) + modTime, _ := os.Stat(backend.TargetLocationFormat(file)) modList = append(modList, modTime.ModTime().Unix()) } @@ -91,7 +90,7 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) string { } // get the full targetLocation path and remove the target - targetLocationComplete := backend.TargetLocationFormat(targetLocationIncomplete[1:]) + targetLocationComplete := backend.TargetLocationFormat(targetLocationIncomplete) if !onServer { // error if target does not exist on client, needed because os.RemoveAll does not return an error if target does not exist backend.TargetIsFile(targetLocationComplete, true, 0) } @@ -105,6 +104,7 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) string { return deviceIDList[0].Name() } return "" + // do not exit program, as this function is used as part of ShearRemoteFromClient } @@ -112,7 +112,7 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) string { // 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 := backend.TargetLocationFormat(targetLocationIncomplete[1:]) + targetLocationComplete := backend.TargetLocationFormat(targetLocationIncomplete) err := os.Mkdir(targetLocationComplete, 0700) if err != nil { if os.IsExist(err) { diff --git a/src/sync/server.go b/src/sync/server.go index 3c95d02..4b8c2a0 100644 --- a/src/sync/server.go +++ b/src/sync/server.go @@ -10,7 +10,7 @@ import ( // GetRemoteDataFromServer prints to stdout the remote entries, mod times, folders, and deletions // lists in output are separated by "\x1d" // output is meant to be captured over SSH for interpretation by the client -func GetRemoteDataFromServer(clientDeviceID string, clientIsWindows bool) { +func GetRemoteDataFromServer(clientDeviceID string) { entryList, dirList := WalkEntryDir() modList := getModTimes(entryList) deletionsList, err := os.ReadDir(backend.ConfigDir + backend.PathSeparator + "deletions") @@ -19,20 +19,11 @@ func GetRemoteDataFromServer(clientDeviceID string, clientIsWindows bool) { os.Exit(1) } - // determine path separator expected by the client - var clientPathSeparator string - switch clientIsWindows { - case false: - clientPathSeparator = "/" - case true: - clientPathSeparator = "\\" - } - // print the lists to stdout // entry list for _, entry := range entryList { - printWithClientPathSeparator(entry, clientPathSeparator, clientIsWindows) + printToStdout(entry) } // modification time list @@ -45,7 +36,7 @@ func GetRemoteDataFromServer(clientDeviceID string, clientIsWindows bool) { // directory/folder list fmt.Print("\x1d") for _, dir := range dirList { - printWithClientPathSeparator(dir, clientPathSeparator, clientIsWindows) + printToStdout(dir) } // deletions list @@ -54,20 +45,10 @@ func GetRemoteDataFromServer(clientDeviceID string, clientIsWindows bool) { // print deletion if it is relevant to the current client device affectedIDTargetLocationIncomplete := strings.Split(deletion.Name(), "\x1d") if affectedIDTargetLocationIncomplete[0] == clientDeviceID { - fmt.Print("\x1f" + strings.ReplaceAll(affectedIDTargetLocationIncomplete[1], "\x1e", backend.PathSeparator)) + fmt.Print("\x1f" + strings.ReplaceAll(affectedIDTargetLocationIncomplete[1], "\x1e", "/")) // do not use printToStdout as separators are filling in for \x1e // 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(backend.ConfigDir + backend.PathSeparator + "deletions" + backend.PathSeparator + deletion.Name()) } } } - -// printWithClientPathSeparator prints the given string with the client's path separator -// it does not alter the string if the client and server are of the same OS family -func printWithClientPathSeparator(printable, clientPathSeparator string, clientIsWindows bool) { - if clientIsWindows == backend.IsWindows { - fmt.Print("\x1f" + printable) - } else { - fmt.Print("\x1f" + strings.ReplaceAll(printable, backend.PathSeparator, clientPathSeparator)) - } -} diff --git a/src/sync/serverPrintToStdoutUNIX.go b/src/sync/serverPrintToStdoutUNIX.go new file mode 100644 index 0000000..b1979c4 --- /dev/null +++ b/src/sync/serverPrintToStdoutUNIX.go @@ -0,0 +1,12 @@ +//go:build !windows + +package sync + +import ( + "fmt" +) + +// printToStdout prints a string to stdout with UNIX path separators +func printToStdout(targetLocationIncomplete string) { + fmt.Print("\x1f" + targetLocationIncomplete) +} diff --git a/src/sync/serverPrintToStdoutWIN.go b/src/sync/serverPrintToStdoutWIN.go new file mode 100644 index 0000000..2f8bd70 --- /dev/null +++ b/src/sync/serverPrintToStdoutWIN.go @@ -0,0 +1,13 @@ +//go:build windows + +package sync + +import ( + "fmt" + "strings" +) + +// printToStdout prints a string to stdout with UNIX path separators +func printToStdout(targetLocationIncomplete string) { + fmt.Print("\x1f" + strings.ReplaceAll(targetLocationIncomplete, "\\", "/")) +}