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 97ba7da6e4
commit 70eba7cec3
7 changed files with 48 additions and 47 deletions
+3 -3
View File
@@ -4,7 +4,7 @@ function cliMUTNEntryCompleter {
$mutnPath = (Resolve-Path '~\AppData\Local\libmutton\entries').Path
try {
$trimmedPaths = If (Test-Path $mutnPath) {
(Get-ChildItem -Path $mutnPath -Recurse -File).FullName.Substring($mutnPath.Length) -replace '\\', '/' -replace ' ', [char]0x23bd
(Get-ChildItem -Path $mutnPath -Recurse -File).FullName.Substring($mutnPath.Length) -replace '\\', '/' -replace ' ', [char]0x259d
}
} catch {
$trimmedPaths = $null # if any errors occur (especially, "You cannot call a method on a null-valued expression", set $trimmedPaths to $null
@@ -50,6 +50,6 @@ function mutn {
[string]$option
)
#Invoke-Expression -Command ('/usr/local/bin/mutn ' + ($entry -replace ' ', '` ' -replace [char]0x1a, '` '), $argument, $option).Trim() # UNIX testing
Invoke-Expression -Command ('mutn.exe ' + ($entry -replace ' ', '` ' -replace [char]0x23bd, '` '), $argument, $option).Trim()
#Invoke-Expression -Command ('/usr/local/bin/mutn ' + ($entry -replace ' ', '` ' -replace [char]0x259d, '` '), $argument, $option).Trim() # UNIX testing
Invoke-Expression -Command ('mutn.exe ' + ($entry -replace ' ', '` ' -replace [char]0x259d, '` '), $argument, $option).Trim()
}
+8 -13
View File
@@ -11,11 +11,6 @@ import (
"strings"
)
// Field separator key:
// \x1d = path separator
// \x1e = space/list separator
// \x1f = misc. field separator (if \x1e is already used)
func main() {
args := os.Args
if len(args) < 2 {
@@ -43,25 +38,25 @@ func main() {
case "rename":
// move an entry to a new location before using fallthrough to add its previous iteration to the deletions directory
// stdin[0] is evaluated after fallthrough
// stdin[1] is expected to be the OLD incomplete target location with "\x1d" representing path separators - always pass in UNIX format
// stdin[2] is expected to be the NEW incomplete target location with "\x1d" representing path separators - always pass in UNIX format
sync.RenameLocal(strings.ReplaceAll(stdin[1], "\x1d", "/"), strings.ReplaceAll(stdin[2], "\x1d", "/"))
// stdin[1] is expected to be the OLD incomplete target location with FSPath representing path separators - always pass in UNIX format
// stdin[2] is expected to be the NEW incomplete target location with FSPath representing path separators - always pass in UNIX format
sync.RenameLocal(strings.ReplaceAll(stdin[1], sync.FSPath, "/"), strings.ReplaceAll(stdin[2], sync.FSPath, "/"))
fallthrough // fallthrough to add the old entry to the deletions directory
case "shear":
// shear an entry from the server and add it to the deletions directory
// stdin[0] is expected to be the device ID
// stdin[1] is expected to be the incomplete target location with "\x1d" representing path separators - always pass in UNIX format
sync.ShearLocal(strings.ReplaceAll(stdin[1], "\x1d", "/"), stdin[0])
// stdin[1] is expected to be the incomplete target location with FSPath representing path separators - always pass in UNIX format
sync.ShearLocal(strings.ReplaceAll(stdin[1], sync.FSPath, "/"), stdin[0])
case "addfolder":
// add a new folder to the server
// stdin[0] is expected to be the incomplete target location with "\x1d" representing path separators - always pass in UNIX format
sync.AddFolderLocal(strings.ReplaceAll(stdin[0], "\x1d", "/"))
// stdin[0] is expected to be the incomplete target location with FSPath representing path separators - always pass in UNIX format
sync.AddFolderLocal(strings.ReplaceAll(stdin[0], sync.FSPath, "/"))
case "register":
// register a new device ID
// stdin[0] is expected to be the device ID
_, _ = os.Create(backend.ConfigDir + backend.PathSeparator + "devices" + backend.PathSeparator + stdin[0]) // error ignored; failure unlikely to occur if init was successful; "register" is not a user-facing argument and thus the error would not be visible
// print EntryRoot and bool indicating OS type to stdout for client to store in config
fmt.Print(backend.EntryRoot + "\x1e" + strconv.FormatBool(backend.IsWindows))
fmt.Print(backend.EntryRoot + sync.FSSpace + strconv.FormatBool(backend.IsWindows))
case "init":
// create the necessary directories for libmuttonserver to function
backend.DirInit(false)
+8 -1
View File
@@ -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)
+16 -17
View File
@@ -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)
@@ -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)
@@ -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)
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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]
}
+9 -9
View File
@@ -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