mirror of
https://github.com/rwinkhart/MUTN.git
synced 2026-08-27 20:36:29 -04:00
Replace use of ASCII control characters with unicode characters allowed in Windows filenames (fixes shearing with a Windows server)
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+18
-19
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user