mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-09-05 16:47:22 -04:00
Lay groundwork for NT/UNIX-like sync interoperability
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
package backend
|
package backend
|
||||||
|
|
||||||
// TargetLocationFormat returns the target location of an entry formatted for the current platform
|
// TargetLocationFormat returns the full location of an entry (given the name) formatted for the current platform
|
||||||
func TargetLocationFormat(entryName string) string {
|
func TargetLocationFormat(targetLocationIncomplete string) string {
|
||||||
return EntryRoot + PathSeparator + entryName
|
return EntryRoot + targetLocationIncomplete
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package backend
|
|||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
// TargetLocationFormat returns the target location of an entry formatted for the current platform
|
// TargetLocationFormat returns the full location of an entry (given the name) formatted for the current platform
|
||||||
func TargetLocationFormat(entryName string) string {
|
func TargetLocationFormat(targetLocationIncomplete string) string {
|
||||||
return EntryRoot + PathSeparator + strings.ReplaceAll(entryName, "/", PathSeparator)
|
return EntryRoot + strings.ReplaceAll(targetLocationIncomplete, "/", PathSeparator)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package sync
|
|
||||||
|
|
||||||
const bareEntryRoot = "/.local/share/libmutton"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package sync
|
|
||||||
|
|
||||||
const bareEntryRoot = "\\AppData\\Local\\libmutton\\entries"
|
|
||||||
+38
-13
@@ -92,6 +92,7 @@ func getSSHClient(manualSync bool) (*ssh.Client, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSSHOutput runs a command over SSH and returns the output as a 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 {
|
func GetSSHOutput(cmd string, manualSync bool) string {
|
||||||
sshClient, _ := getSSHClient(manualSync)
|
sshClient, _ := getSSHClient(manualSync)
|
||||||
defer sshClient.Close()
|
defer sshClient.Close()
|
||||||
@@ -178,6 +179,15 @@ func getLocalData() map[string]int64 {
|
|||||||
return entryModMap
|
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
|
// 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) {
|
func sftpSync(downloadList, uploadList []string, manualSync bool) {
|
||||||
// establish an SSH connection for transfers
|
// establish an SSH connection for transfers
|
||||||
@@ -199,9 +209,12 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
|
|||||||
|
|
||||||
fmt.Println("Downloading " + ansiDownload + entryName + backend.AnsiReset)
|
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
|
// save modification time of remote file
|
||||||
var fileInfo os.FileInfo
|
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 {
|
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 (modtime):", err.Error()+backend.AnsiReset)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -210,15 +223,18 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
|
|||||||
|
|
||||||
// open remote file
|
// open remote file
|
||||||
var remoteFile *sftp.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 {
|
if err != nil {
|
||||||
fmt.Println(backend.AnsiError+"Sync failed - Unable to open remote file:", err.Error()+backend.AnsiReset)
|
fmt.Println(backend.AnsiError+"Sync failed - Unable to open remote file:", err.Error()+backend.AnsiReset)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store path to local entry
|
||||||
|
localEntryFullPath := backend.TargetLocationFormat(entryName)
|
||||||
|
|
||||||
// create local file
|
// create local file
|
||||||
var localFile *os.File
|
var localFile *os.File
|
||||||
localFile, err = os.Create(backend.EntryRoot + entryName)
|
localFile, err = os.Create(localEntryFullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(backend.AnsiError+"Sync failed - Unable to create local file:", err.Error()+backend.AnsiReset)
|
fmt.Println(backend.AnsiError+"Sync failed - Unable to create local file:", err.Error()+backend.AnsiReset)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -236,7 +252,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
|
|||||||
localFile.Close()
|
localFile.Close()
|
||||||
|
|
||||||
// set the modification time of the local file to match the value saved from the remote file (from before the download)
|
// 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 {
|
if filesTransfered {
|
||||||
@@ -250,9 +266,12 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
|
|||||||
|
|
||||||
fmt.Println("Uploading " + ansiUpload + entryName + backend.AnsiReset)
|
fmt.Println("Uploading " + ansiUpload + entryName + backend.AnsiReset)
|
||||||
|
|
||||||
|
// store path to local entry
|
||||||
|
localEntryFullPath := backend.TargetLocationFormat(entryName)
|
||||||
|
|
||||||
// save modification time of local file
|
// save modification time of local file
|
||||||
var fileInfo os.FileInfo
|
var fileInfo os.FileInfo
|
||||||
fileInfo, err = os.Stat(backend.EntryRoot + entryName)
|
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 (modtime):", err.Error()+backend.AnsiReset)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -261,15 +280,18 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
|
|||||||
|
|
||||||
// open local file
|
// open local file
|
||||||
var localFile *os.File
|
var localFile *os.File
|
||||||
localFile, err = os.Open(backend.EntryRoot + entryName)
|
localFile, err = os.Open(localEntryFullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(backend.AnsiError+"Sync failed - Unable to open local file:", err.Error()+backend.AnsiReset)
|
fmt.Println(backend.AnsiError+"Sync failed - Unable to open local file:", err.Error()+backend.AnsiReset)
|
||||||
os.Exit(1)
|
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
|
// create remote file
|
||||||
var remoteFile *sftp.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 {
|
if err != nil {
|
||||||
fmt.Println(backend.AnsiError+"Sync failed - Unable to create remote file:", err.Error()+backend.AnsiReset)
|
fmt.Println(backend.AnsiError+"Sync failed - Unable to create remote file:", err.Error()+backend.AnsiReset)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -287,7 +309,7 @@ func sftpSync(downloadList, uploadList []string, manualSync bool) {
|
|||||||
remoteFile.Close()
|
remoteFile.Close()
|
||||||
|
|
||||||
// set the modification time of the remote file to match the value saved from the local file (from before the upload)
|
// 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 {
|
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
|
// 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?
|
// 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
|
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 {
|
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)
|
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)")
|
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 {
|
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
|
// AddFolderRemoteFromClient creates a new entry-containing directory on the local system and calls the server to create the folder remotely
|
||||||
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, " ", "\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)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
@@ -373,11 +395,14 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string) {
|
|||||||
// folderSync creates folders on the client (from the given list of folder names)
|
// folderSync creates folders on the client (from the given list of folder names)
|
||||||
func folderSync(folders []string) {
|
func folderSync(folders []string) {
|
||||||
for _, folder := range folders {
|
for _, folder := range folders {
|
||||||
|
// store the full local path of the folder
|
||||||
|
folderFullPath := backend.TargetLocationFormat(folder)
|
||||||
|
|
||||||
// check if folder already exists
|
// check if folder already exists
|
||||||
isFile, isAccessible := backend.TargetIsFile(backend.EntryRoot+folder, false, 1)
|
isFile, isAccessible := backend.TargetIsFile(folderFullPath, false, 1)
|
||||||
|
|
||||||
if !isFile && !isAccessible {
|
if !isFile && !isAccessible {
|
||||||
os.MkdirAll(backend.EntryRoot+folder, 0700)
|
os.MkdirAll(folderFullPath, 0700)
|
||||||
} else if isFile {
|
} else if isFile {
|
||||||
fmt.Println(backend.AnsiError + "Sync failed - Failed to create folder \"" + folder + "\" - a file with the same name already exists" + backend.AnsiReset)
|
fmt.Println(backend.AnsiError + "Sync failed - Failed to create folder \"" + folder + "\" - a file with the same name already exists" + backend.AnsiReset)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
+4
-4
@@ -10,7 +10,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// WalkEntryDir walks the entry directory and returns lists of all files and directories found (two separate lists)
|
// 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) {
|
func WalkEntryDir() ([]string, []string) {
|
||||||
// define file/directory containing slices so that they may be accessed by the anonymous WalkDir function
|
// define file/directory containing slices so that they may be accessed by the anonymous WalkDir function
|
||||||
var fileList []string
|
var fileList []string
|
||||||
@@ -51,7 +50,7 @@ func getModTimes(entryList []string) []int64 {
|
|||||||
// get a list of all entry modification times
|
// get a list of all entry modification times
|
||||||
var modList []int64
|
var modList []int64
|
||||||
for _, file := range entryList {
|
for _, file := range entryList {
|
||||||
modTime, _ := os.Stat(backend.EntryRoot + file)
|
modTime, _ := os.Stat(backend.TargetLocationFormat(file))
|
||||||
modList = append(modList, modTime.ModTime().Unix())
|
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
|
// 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
|
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)
|
backend.TargetIsFile(targetLocationComplete, true, 0)
|
||||||
}
|
}
|
||||||
@@ -105,6 +104,7 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) string {
|
|||||||
return deviceIDList[0].Name()
|
return deviceIDList[0].Name()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
// do not exit program, as this function is used as part of ShearRemoteFromClient
|
// 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
|
// this function should only be used directly by the server binary
|
||||||
func AddFolderLocal(targetLocationIncomplete string) {
|
func AddFolderLocal(targetLocationIncomplete string) {
|
||||||
// get the full targetLocation path and create the target
|
// get the full targetLocation path and create the target
|
||||||
targetLocationComplete := backend.TargetLocationFormat(targetLocationIncomplete[1:])
|
targetLocationComplete := backend.TargetLocationFormat(targetLocationIncomplete)
|
||||||
err := os.Mkdir(targetLocationComplete, 0700)
|
err := os.Mkdir(targetLocationComplete, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsExist(err) {
|
if os.IsExist(err) {
|
||||||
|
|||||||
+4
-23
@@ -10,7 +10,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 "\x1d"
|
// lists in output are separated by "\x1d"
|
||||||
// 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, clientIsWindows bool) {
|
func GetRemoteDataFromServer(clientDeviceID string) {
|
||||||
entryList, dirList := WalkEntryDir()
|
entryList, dirList := WalkEntryDir()
|
||||||
modList := getModTimes(entryList)
|
modList := getModTimes(entryList)
|
||||||
deletionsList, err := os.ReadDir(backend.ConfigDir + backend.PathSeparator + "deletions")
|
deletionsList, err := os.ReadDir(backend.ConfigDir + backend.PathSeparator + "deletions")
|
||||||
@@ -19,20 +19,11 @@ func GetRemoteDataFromServer(clientDeviceID string, clientIsWindows bool) {
|
|||||||
os.Exit(1)
|
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
|
// print the lists to stdout
|
||||||
|
|
||||||
// entry list
|
// entry list
|
||||||
for _, entry := range entryList {
|
for _, entry := range entryList {
|
||||||
printWithClientPathSeparator(entry, clientPathSeparator, clientIsWindows)
|
printToStdout(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// modification time list
|
// modification time list
|
||||||
@@ -45,7 +36,7 @@ func GetRemoteDataFromServer(clientDeviceID string, clientIsWindows bool) {
|
|||||||
// directory/folder list
|
// directory/folder list
|
||||||
fmt.Print("\x1d")
|
fmt.Print("\x1d")
|
||||||
for _, dir := range dirList {
|
for _, dir := range dirList {
|
||||||
printWithClientPathSeparator(dir, clientPathSeparator, clientIsWindows)
|
printToStdout(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deletions list
|
// deletions list
|
||||||
@@ -54,20 +45,10 @@ func GetRemoteDataFromServer(clientDeviceID string, clientIsWindows bool) {
|
|||||||
// 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(), "\x1d")
|
affectedIDTargetLocationIncomplete := strings.Split(deletion.Name(), "\x1d")
|
||||||
if affectedIDTargetLocationIncomplete[0] == clientDeviceID {
|
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)
|
// 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())
|
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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, "\\", "/"))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user