From a2a92985afb7c3a2c1bb4d49695f425e26ec398a Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Wed, 10 Jul 2024 15:23:13 -0400 Subject: [PATCH] Replace use of ASCII control characters with unicode characters allowed in Windows filenames (fixes shearing with a Windows server) --- src/sync/1globals.go | 9 ++++++++- src/sync/client.go | 37 ++++++++++++++++++------------------- src/sync/common.go | 2 +- src/sync/init.go | 2 +- src/sync/server.go | 18 +++++++++--------- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/sync/1globals.go b/src/sync/1globals.go index 2a2a0dd..03063fc 100644 --- a/src/sync/1globals.go +++ b/src/sync/1globals.go @@ -2,5 +2,12 @@ package sync import "github.com/rwinkhart/MUTN/src/backend" -// RootLength store length of backend.EntryRoot string +// define field separator constants +const ( + FSSpace = "\u259d" // ▝ space/list separator + FSPath = "\u259e" // ▞ path separator + FSMisc = "\u259f" // ▟ misc. field separator (if \u259d is already used) +) + +// rootLength stores length of backend.EntryRoot string var rootLength = len(backend.EntryRoot) diff --git a/src/sync/client.go b/src/sync/client.go index 5b06bdc..836ce7b 100644 --- a/src/sync/client.go +++ b/src/sync/client.go @@ -143,18 +143,18 @@ func getRemoteDataFromClient(manualSync bool) (map[string]int64, []string, []str } output := GetSSHOutput("libmuttonserver fetch", clientDeviceID[0].Name(), manualSync) - // split output into slice based on occurrences of "\x1e" - outputSlice := strings.Split(output, "\x1e") + // split output into slice based on occurrences of FSSpace + outputSlice := strings.Split(output, FSSpace) // re-form the lists if len(outputSlice) != 4 { // ensure information from server is complete fmt.Println(backend.AnsiError + "Sync failed - Unable to fetch remote data; server returned an unexpected response" + backend.AnsiReset) os.Exit(1) } - entries := strings.Split(outputSlice[0], "\x1f")[1:] - modsStrings := strings.Split(outputSlice[1], "\x1f")[1:] - folders := strings.Split(outputSlice[2], "\x1f")[1:] - deletions := strings.Split(outputSlice[3], "\x1f")[1:] + entries := strings.Split(outputSlice[0], FSMisc)[1:] + modsStrings := strings.Split(outputSlice[1], FSMisc)[1:] + folders := strings.Split(outputSlice[2], FSMisc)[1:] + deletions := strings.Split(outputSlice[3], FSMisc)[1:] // convert the mod times to int64 var mods []int64 @@ -200,7 +200,6 @@ func targetLocationFormatSFTP(targetName, serverEntryRoot string, serverIsWindow } // sftpSync takes two slices of entries (one for downloads and one for uploads) and syncs them between the client and server using SFTP -// TODO test Windows server hosting support func sftpSync(downloadList, uploadList []string, manualSync bool) { // establish an SSH connection for transfers sshClient, sshEntryRoot, sshIsWindows := getSSHClient(manualSync) @@ -215,9 +214,9 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { defer sftpClient.Close() // iterate over the download list - var filesTransfered bool + var filesTransferred bool for _, entryName := range downloadList { - filesTransfered = true // set a flag to indicate that files have been downloaded (used to determine whether to print a gap between download and upload messages) + 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 + backend.AnsiReset) @@ -228,7 +227,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { var fileInfo os.FileInfo 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) + fmt.Println(backend.AnsiError+"Sync failed - Unable to get remote file info (mod time):", err.Error()+backend.AnsiReset) os.Exit(1) } modTime := fileInfo.ModTime() @@ -267,14 +266,14 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { err = os.Chtimes(localEntryFullPath, time.Now(), modTime) } - if filesTransfered { + if filesTransferred { fmt.Println() // add a gap between download and upload messages } // iterate over the upload list - filesTransfered = false + filesTransferred = false for _, entryName := range uploadList { - filesTransfered = 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) + 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 + backend.AnsiReset) @@ -285,7 +284,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { var fileInfo os.FileInfo 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) + fmt.Println(backend.AnsiError+"Sync failed - Unable to get local file info (mod time):", err.Error()+backend.AnsiReset) os.Exit(1) } modTime := fileInfo.ModTime() @@ -324,7 +323,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) { err = sftpClient.Chtimes(remoteEntryFullPath, time.Now(), modTime) } - if filesTransfered { + if filesTransferred { fmt.Println() // add a gap between upload and sync complete messages } } @@ -392,7 +391,7 @@ func ShearRemoteFromClient(targetLocationIncomplete string) { if deviceID != "" { // ensure a device ID exists (online mode) // call the server to remotely shear the target and add it to the deletions list GetSSHOutput("libmuttonserver shear", deviceID+"\n"+ - strings.ReplaceAll(targetLocationIncomplete, backend.PathSeparator, "\x1d"), false) + strings.ReplaceAll(targetLocationIncomplete, backend.PathSeparator, FSPath), false) } backend.Exit(0) // sync is not required after shearing since the target has already been removed from the local system @@ -408,8 +407,8 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string) // call the server to move the target on the remote system and add the old target to the deletions list GetSSHOutput("libmuttonserver rename", (*deviceIDList)[0].Name()+"\n"+ - strings.ReplaceAll(oldLocationIncomplete, backend.PathSeparator, "\x1d")+"\n"+ - strings.ReplaceAll(newLocationIncomplete, backend.PathSeparator, "\x1d"), false) + strings.ReplaceAll(oldLocationIncomplete, backend.PathSeparator, FSPath)+"\n"+ + strings.ReplaceAll(newLocationIncomplete, backend.PathSeparator, FSPath), false) } backend.Exit(0) @@ -419,7 +418,7 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string) // 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) { AddFolderLocal(targetLocationIncomplete) // add the folder on the local system - GetSSHOutput("libmuttonserver addfolder", strings.ReplaceAll(targetLocationIncomplete, backend.PathSeparator, "\x1d"), false) // call the server to create the folder remotely + GetSSHOutput("libmuttonserver addfolder", strings.ReplaceAll(targetLocationIncomplete, backend.PathSeparator, FSPath), false) // call the server to create the folder remotely backend.Exit(0) } diff --git a/src/sync/common.go b/src/sync/common.go index 110997d..99fc303 100644 --- a/src/sync/common.go +++ b/src/sync/common.go @@ -47,7 +47,7 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) string { if onServer { for _, device := range *deviceIDList { if device.Name() != clientDeviceID { - _, err := os.Create(backend.ConfigDir + backend.PathSeparator + "deletions" + backend.PathSeparator + device.Name() + "\x1e" + strings.ReplaceAll(targetLocationIncomplete, "/", "\x1d")) + _, err := os.Create(backend.ConfigDir + backend.PathSeparator + "deletions" + backend.PathSeparator + device.Name() + FSSpace + strings.ReplaceAll(targetLocationIncomplete, "/", FSPath)) 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) diff --git a/src/sync/init.go b/src/sync/init.go index 4c91539..2e77484 100644 --- a/src/sync/init.go +++ b/src/sync/init.go @@ -25,7 +25,7 @@ func DeviceIDGen() (string, string) { // register device ID with server and fetch remote EntryRoot and OS type //manualSync is true so the user is alerted if device ID registration fails - sshEntryRootSSHIsWindows := strings.Split(GetSSHOutput("libmuttonserver register", deviceID, true), "\x1e") + sshEntryRootSSHIsWindows := strings.Split(GetSSHOutput("libmuttonserver register", deviceID, true), FSSpace) return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1] } diff --git a/src/sync/server.go b/src/sync/server.go index b477181..1677bcd 100644 --- a/src/sync/server.go +++ b/src/sync/server.go @@ -8,7 +8,7 @@ import ( ) // GetRemoteDataFromServer prints to stdout the remote entries, mod times, folders, and deletions -// lists in output are separated by "\x1e" +// 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() @@ -23,29 +23,29 @@ func GetRemoteDataFromServer(clientDeviceID string) { // entry list for _, entry := range entryList { - fmt.Print("\x1f" + entry) + fmt.Print(FSMisc + entry) } // modification time list - fmt.Print("\x1e") + fmt.Print(FSSpace) for _, mod := range modList { - fmt.Print("\x1f") + fmt.Print(FSMisc) fmt.Print(mod) } // directory/folder list - fmt.Print("\x1e") + fmt.Print(FSSpace) for _, dir := range dirList { - fmt.Print("\x1f" + dir) + fmt.Print(FSMisc + dir) } // deletions list - fmt.Print("\x1e") + fmt.Print(FSSpace) for _, deletion := range deletionsList { // print deletion if it is relevant to the current client device - affectedIDTargetLocationIncomplete := strings.Split(deletion.Name(), "\x1e") + affectedIDTargetLocationIncomplete := strings.Split(deletion.Name(), FSSpace) if affectedIDTargetLocationIncomplete[0] == clientDeviceID { - fmt.Print("\x1f" + strings.ReplaceAll(affectedIDTargetLocationIncomplete[1], "\x1d", "/")) + fmt.Print(FSMisc + strings.ReplaceAll(affectedIDTargetLocationIncomplete[1], 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(backend.ConfigDir + backend.PathSeparator + "deletions" + backend.PathSeparator + deletion.Name()) // error ignored; function not run from a user-facing argument and thus the error would not be visible