Replace use of ASCII control characters with unicode characters allowed in Windows filenames (fixes shearing with a Windows server)

This commit is contained in:
2024-07-10 15:23:13 -04:00
parent 6b3a136c5e
commit a2a92985af
5 changed files with 37 additions and 31 deletions
+8 -1
View File
@@ -2,5 +2,12 @@ package sync
import "github.com/rwinkhart/MUTN/src/backend" 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) var rootLength = len(backend.EntryRoot)
+18 -19
View File
@@ -143,18 +143,18 @@ func getRemoteDataFromClient(manualSync bool) (map[string]int64, []string, []str
} }
output := GetSSHOutput("libmuttonserver fetch", clientDeviceID[0].Name(), manualSync) output := GetSSHOutput("libmuttonserver fetch", clientDeviceID[0].Name(), manualSync)
// split output into slice based on occurrences of "\x1e" // split output into slice based on occurrences of FSSpace
outputSlice := strings.Split(output, "\x1e") outputSlice := strings.Split(output, FSSpace)
// re-form the lists // re-form the lists
if len(outputSlice) != 4 { // ensure information from server is complete 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) fmt.Println(backend.AnsiError + "Sync failed - Unable to fetch remote data; server returned an unexpected response" + backend.AnsiReset)
os.Exit(1) os.Exit(1)
} }
entries := strings.Split(outputSlice[0], "\x1f")[1:] entries := strings.Split(outputSlice[0], FSMisc)[1:]
modsStrings := strings.Split(outputSlice[1], "\x1f")[1:] modsStrings := strings.Split(outputSlice[1], FSMisc)[1:]
folders := strings.Split(outputSlice[2], "\x1f")[1:] folders := strings.Split(outputSlice[2], FSMisc)[1:]
deletions := strings.Split(outputSlice[3], "\x1f")[1:] deletions := strings.Split(outputSlice[3], FSMisc)[1:]
// convert the mod times to int64 // convert the mod times to int64
var mods []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 // 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) { func sftpSync(downloadList, uploadList []string, manualSync bool) {
// establish an SSH connection for transfers // establish an SSH connection for transfers
sshClient, sshEntryRoot, sshIsWindows := getSSHClient(manualSync) sshClient, sshEntryRoot, sshIsWindows := getSSHClient(manualSync)
@@ -215,9 +214,9 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
defer sftpClient.Close() defer sftpClient.Close()
// iterate over the download list // iterate over the download list
var filesTransfered bool var filesTransferred bool
for _, entryName := range downloadList { 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) fmt.Println("Downloading " + ansiDownload + entryName + backend.AnsiReset)
@@ -228,7 +227,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
var fileInfo os.FileInfo var fileInfo os.FileInfo
fileInfo, err = sftpClient.Stat(remoteEntryFullPath) fileInfo, err = sftpClient.Stat(remoteEntryFullPath)
if err != nil { 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) os.Exit(1)
} }
modTime := fileInfo.ModTime() modTime := fileInfo.ModTime()
@@ -267,14 +266,14 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
err = os.Chtimes(localEntryFullPath, time.Now(), modTime) err = os.Chtimes(localEntryFullPath, time.Now(), modTime)
} }
if filesTransfered { if filesTransferred {
fmt.Println() // add a gap between download and upload messages fmt.Println() // add a gap between download and upload messages
} }
// iterate over the upload list // iterate over the upload list
filesTransfered = false filesTransferred = false
for _, entryName := range uploadList { 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) fmt.Println("Uploading " + ansiUpload + entryName + backend.AnsiReset)
@@ -285,7 +284,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
var fileInfo os.FileInfo var fileInfo os.FileInfo
fileInfo, err = os.Stat(localEntryFullPath) fileInfo, err = os.Stat(localEntryFullPath)
if err != nil { 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) os.Exit(1)
} }
modTime := fileInfo.ModTime() modTime := fileInfo.ModTime()
@@ -324,7 +323,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
err = sftpClient.Chtimes(remoteEntryFullPath, time.Now(), modTime) err = sftpClient.Chtimes(remoteEntryFullPath, time.Now(), modTime)
} }
if filesTransfered { if filesTransferred {
fmt.Println() // add a gap between upload and sync complete messages 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) if deviceID != "" { // ensure a device ID exists (online mode)
// call the server to remotely shear the target and add it to the deletions list // call the server to remotely shear the target and add it to the deletions list
GetSSHOutput("libmuttonserver shear", deviceID+"\n"+ 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 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 // call the server to move the target on the remote system and add the old target to the deletions list
GetSSHOutput("libmuttonserver rename", GetSSHOutput("libmuttonserver rename",
(*deviceIDList)[0].Name()+"\n"+ (*deviceIDList)[0].Name()+"\n"+
strings.ReplaceAll(oldLocationIncomplete, backend.PathSeparator, "\x1d")+"\n"+ strings.ReplaceAll(oldLocationIncomplete, backend.PathSeparator, FSPath)+"\n"+
strings.ReplaceAll(newLocationIncomplete, backend.PathSeparator, "\x1d"), false) strings.ReplaceAll(newLocationIncomplete, backend.PathSeparator, FSPath), false)
} }
backend.Exit(0) 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) // 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) { func AddFolderRemoteFromClient(targetLocationIncomplete string) {
AddFolderLocal(targetLocationIncomplete) // add the folder on the local system 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) backend.Exit(0)
} }
+1 -1
View File
@@ -47,7 +47,7 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) string {
if onServer { if onServer {
for _, device := range *deviceIDList { for _, device := range *deviceIDList {
if device.Name() != clientDeviceID { 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 { if err != nil {
// do not print error as there is currently no way of seeing server-side errors // 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) // failure to add the target to the deletions list will exit the program and result in a client re-uploading the target (non-critical)
+1 -1
View File
@@ -25,7 +25,7 @@ func DeviceIDGen() (string, string) {
// register device ID with server and fetch remote EntryRoot and OS type // 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 //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] return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1]
} }
+9 -9
View File
@@ -8,7 +8,7 @@ import (
) )
// GetRemoteDataFromServer prints to stdout the remote entries, mod times, folders, and deletions // 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 // output is meant to be captured over SSH for interpretation by the client
func GetRemoteDataFromServer(clientDeviceID string) { func GetRemoteDataFromServer(clientDeviceID string) {
entryList, dirList := WalkEntryDir() entryList, dirList := WalkEntryDir()
@@ -23,29 +23,29 @@ func GetRemoteDataFromServer(clientDeviceID string) {
// entry list // entry list
for _, entry := range entryList { for _, entry := range entryList {
fmt.Print("\x1f" + entry) fmt.Print(FSMisc + entry)
} }
// modification time list // modification time list
fmt.Print("\x1e") fmt.Print(FSSpace)
for _, mod := range modList { for _, mod := range modList {
fmt.Print("\x1f") fmt.Print(FSMisc)
fmt.Print(mod) fmt.Print(mod)
} }
// directory/folder list // directory/folder list
fmt.Print("\x1e") fmt.Print(FSSpace)
for _, dir := range dirList { for _, dir := range dirList {
fmt.Print("\x1f" + dir) fmt.Print(FSMisc + dir)
} }
// deletions list // deletions list
fmt.Print("\x1e") fmt.Print(FSSpace)
for _, deletion := range deletionsList { for _, deletion := range deletionsList {
// print deletion if it is relevant to the current client device // 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 { 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) // 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 _ = 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