Compare commits

...

8 Commits

Author SHA1 Message Date
Thorsten Sommer
57dcea32ff Bugfix: No Subdirectory 2017-07-02 22:05:55 +02:00
Thorsten Sommer
a57f82a546 Updated 2017-07-02 13:53:45 +02:00
Thorsten Sommer
444efe3d31 Added showHostKey function 2017-07-02 13:45:34 +02:00
Thorsten Sommer
edfdb47f47 v1.3.0 2017-03-04 11:27:20 +01:00
Thorsten Sommer
a97c5dadcc Added file type 2017-03-04 11:27:10 +01:00
Thorsten Sommer
71ba42bec8 Bugfix: Error while deleting remote dirs 2017-03-04 11:26:58 +01:00
Thorsten Sommer
96183890ac Change of platform 2017-02-19 16:20:43 +01:00
Thorsten Sommer
5cd2909677 Version 1.2.0
In case the password must provided interactively, it is now invisible. Added also exit codes in case of errors for a better usage and automation e.g. with Docker containers. Finally, improved the console output for e.g. Docker containers.
2016-03-16 08:03:29 +01:00
18 changed files with 897 additions and 846 deletions

49
.gitignore vendored
View File

@ -1,24 +1,25 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
*.DS_Store

View File

@ -1,9 +1,12 @@
package main
func correctPath(path string) string {
if path[len(path)-1:] == `/` || path[len(path)-1:] == `\` {
return path[:len(path)-1]
} else {
return path
}
}
package main
func correctPath(path string) string {
if path[len(path)-1:] == `/` || path[len(path)-1:] == `\` {
// Case: E.g. remote dir = /myfiles/
return path[:len(path)-1]
} else {
// Case: E.g. remote dir = /myfiles
return path
}
}

48
LICENSE
View File

@ -1,24 +1,24 @@
Copyright (c) 2015, Thorsten Sommer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright (c) 2015, Thorsten Sommer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

32
Main.go
View File

@ -2,17 +2,21 @@ package main
import (
"fmt"
"github.com/SommerEngineering/Sync/Sync"
"golang.org/x/crypto/ssh"
"log"
"net"
"os"
"runtime"
"time"
"github.com/SommerEngineering/Sync/Sync"
"github.com/howeyc/gopass"
"golang.org/x/crypto/ssh"
)
func main() {
// Show the current version:
log.Println(`Sync v1.1.0`)
log.Println(`Sync v1.3.2`)
// Allow Go to use all CPUs:
runtime.GOMAXPROCS(runtime.NumCPU())
@ -23,6 +27,7 @@ func main() {
// Check if the directories are provided:
if localDir == `` || remoteDir == `` {
log.Println(`Please provide the local and remote directory.`)
os.Exit(1)
return
}
@ -30,6 +35,7 @@ func main() {
if localDir == `.` {
if currentWD, currentWDError := os.Getwd(); currentWDError != nil {
log.Println("Cannot use the current working directory as local directory: " + currentWDError.Error())
os.Exit(2)
return
} else {
log.Println("I use the current working directory as local directory: " + currentWD)
@ -44,10 +50,12 @@ func main() {
// Check if local dir exist
if dirInfo, dirError := os.Stat(localDir); dirError != nil {
log.Println("There is an error with the local directory: " + dirError.Error())
os.Exit(3)
return
} else {
if !dirInfo.IsDir() {
log.Println("There is an error with the local directory: You provided a file instead!")
os.Exit(4)
return
}
}
@ -57,7 +65,13 @@ func main() {
if password == `` {
// Promt for the password:
fmt.Print(`Please provide the password for the connection: `)
fmt.Scanln(&password)
if pass, errPass := gopass.GetPasswd(); errPass != nil {
log.Println(`There was an error reading the password securely: ` + errPass.Error())
os.Exit(5)
return
} else {
password = string(pass)
}
} else {
break
}
@ -85,12 +99,14 @@ func main() {
ssh.PasswordCallback(Sync.PasswordCallback),
ssh.KeyboardInteractive(Sync.KeyboardInteractiveChallenge),
},
HostKeyCallback: showHostKey(),
}
// Connect to the SSH server:
ssh := Sync.ConnectSSH(config, serverAddrString)
if ssh == nil {
log.Println(`It was not possible to connect to the SSH server.`)
os.Exit(6)
return
}
@ -98,3 +114,11 @@ func main() {
Sync.Synchronise(ssh, supervised, pushOnly, localDir, remoteDir)
log.Println("Synchronising done.")
}
func showHostKey() ssh.HostKeyCallback {
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
log.Printf("Your server's hostname is %s (%s) and its public key is %s. If this is wrong, please abort the program now! Wait 16 seconds for your check.", hostname, remote.String(), ssh.FingerprintSHA256(key))
time.Sleep(16 * time.Second)
return nil
}
}

View File

@ -1,25 +1,25 @@
# Sync
This is a simple SFTP synchronisation tool **without** any external dependencies e.g. PuTTY, WinSCP, OpenSSH. Just download the executable which matches your OS and architecture (32 vs. 64 bits) and run it.
## Syntax
Sync provids a few arguments:
- ``localDir`` defines the local directory e.g. ``c:\Users\A\My Documents`` or ``/users/A/My Documents``. If you omit or set it to ``.``, the current working directory gets used.
- ``remoteDir`` is the remote directory e.g. ``/users/A/Sync``.
- ``server`` is the SSH server e.g. ``my-server.com:22``.
- Use ``user`` to set the user's name and ``pwd`` to set the user's password. You can omit the ``pwd``, thus, the program ask for the password on demand.
- With the argument ``supervised`` you can control if the program should ask you for every type of change. The default is the supervised operating mode. In order to disable it, use ``supervised=false``.
- Finnaly, the ``pushOnly`` argument can be used to set the **backup mode**, where all remote changes are ignored. The default is the enabled backup mode. In order to sync changes in both directions, set ``pushOnly=false``.
## Features
- The whole code is open source and can be used for any purpose (also commercial)
- If you want, you can compile the code by your own by using the [Go](http://www.golang.org)
- The program just needs very low resources e.g. around 14 MB memory for Microsoft Windows 10 to sync roughly 6000 files. More files need more memory, because the meta information must keep in memory.
- If a connection cannot setup, the program re-tries it
- At the moment, SSHTunnel uses only the password authentication methods. Therefore, it is currently not possible to use e.g. a certificate, etc. Nevertheless, the implementation of this feature is possible.
- The configuration must be provided by using the command-line arguments. It is currently not possible to use e.g. a configuration file.
- You can avoid the password argument if you prefer to provide the password on demand.
- Sync can be used in **backup mode** where all remote changes are ignored or in the **full mode**, where both directions gets synchronised.
- Use the **supervised mode** in order to get the full control
## Download
Go and get the latest release from the [release page](https://github.com/SommerEngineering/Sync/releases). Sync use the [SFTP library](https://github.com/pkg/sftp) from Dave Cheney. Thanks very much for the good work!
# Sync
This is a simple SFTP synchronisation tool **without** any external dependencies e.g. PuTTY, WinSCP, OpenSSH. Just download the executable which matches your OS and architecture (32 vs. 64 bits) and run it.
## Syntax
Sync uses several parameters:
- The `localDir` parameter defines the local directory e.g. `c:\Users\A\My Documents` or `/users/A/My Documents`. If you omit or set it to `.`, the current working directory gets used.
- The `remoteDir` parameter sets the remote directory e.g. `/users/A/Sync`.
- The `server` parameter is the SSH server e.g. `my-server.com:22`.
- Use the `user` parameter to set the user's name and `pwd` to set the user's password. You can omit the `pwd` and the program ask for the password on demand.
- The parameter `supervised` controls if the program should ask you for every type of change. The default is the supervised operating mode. In order to disable it, use `supervised=false`.
- Finnaly, the `pushOnly` argument can be used to set the **backup mode**, where all remote changes are ignored, which is the default. In order to sync changes in both directions, use `pushOnly=false`.
## Features
- The whole code is open source and can be used for any purpose (also commercial)
- If you want, you can compile the code by your own by using the [Go](http://www.golang.org)
- The program needs very low resources e.g. around 14 MB of memory on Microsoft Windows 10 to sync roughly 6,000 files. More files need more memory, because the meta information must keep in memory.
- If a connection cannot set up, the program re-tries it.
- At the moment, Sync uses only the password authentication methods. Therefore, it is currently not possible to use e.g. a certificate, etc. Nevertheless, the implementation of this feature is possible.
- The configuration must be provided by using the command-line arguments. It is currently not possible to use e.g. a configuration file.
- You can avoid the password argument if you prefer to provide the password on demand.
- Sync can be used in **backup mode** where all remote changes are ignored or in the **full mode**, where both directions gets synchronized.
- Use the **supervised mode** in order to get the full control about any change.
## Download
Go and get the latest release from the [release page](https://github.com/SommerEngineering/Sync/releases). Sync use the [SFTP library](https://github.com/pkg/sftp) from Dave Cheney. Thanks very much for the great work!

View File

@ -1,16 +1,16 @@
package main
import (
"flag"
)
func readFlags() {
flag.StringVar(&serverAddrString, `server`, `127.0.0.1:22`, `The (remote) SSH server, e.g. 'my.host.com', 'my.host.com:22', '127.0.0.1:22', 'localhost:22'.`)
flag.StringVar(&username, `user`, `username`, `The user's name for the SSH server.`)
flag.StringVar(&password, `pwd`, ``, `The user's password for the SSH server. You can omit these argument. Thus, the program asks for the password on demand.`)
flag.StringVar(&localDir, `localDir`, `.`, `The local directory which should be synced. Use . for the current working directory.`)
flag.StringVar(&remoteDir, `remoteDir`, ``, `The remote directory which should be synced.`)
flag.BoolVar(&supervised, `supervised`, true, `Use the supervised mode? The algorithm asks you before any change.`)
flag.BoolVar(&pushOnly, `pushOnly`, true, `Use the push only mode, i.e. backup mode. Ignore any change on the remote side!`)
flag.Parse()
}
package main
import (
"flag"
)
func readFlags() {
flag.StringVar(&serverAddrString, `server`, `127.0.0.1:22`, `The (remote) SSH server, e.g. 'my.host.com', 'my.host.com:22', '127.0.0.1:22', 'localhost:22'.`)
flag.StringVar(&username, `user`, `username`, `The user's name for the SSH server.`)
flag.StringVar(&password, `pwd`, ``, `The user's password for the SSH server. You can omit these argument. Thus, the program asks for the password on demand.`)
flag.StringVar(&localDir, `localDir`, `.`, `The local directory which should be synced. Use . for the current working directory.`)
flag.StringVar(&remoteDir, `remoteDir`, ``, `The remote directory which should be synced.`)
flag.BoolVar(&supervised, `supervised`, true, `Use the supervised mode? The algorithm asks you before any change.`)
flag.BoolVar(&pushOnly, `pushOnly`, true, `Use the push only mode, i.e. backup mode. Ignore any change on the remote side!`)
flag.Parse()
}

16
Sync/ByLength.go Normal file
View File

@ -0,0 +1,16 @@
package Sync
type ByLength []string
func (s ByLength) Len() int {
return len(s)
}
func (s ByLength) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Longer strings will appear first.
func (s ByLength) Less(i, j int) bool {
return len(s[i]) > len(s[j])
}

View File

@ -1,7 +1,7 @@
package Sync
func comparePath(localBase, localPath, remoteBase, remotePath string) bool {
localCompare := normalisePath(localBase, localPath)
remoteCompare := normalisePath(remoteBase, remotePath)
return localCompare == remoteCompare
}
package Sync
func comparePath(localBase, localPath, remoteBase, remotePath string) bool {
localCompare := normalisePath(localBase, localPath)
remoteCompare := normalisePath(remoteBase, remotePath)
return localCompare == remoteCompare
}

View File

@ -1,42 +1,42 @@
package Sync
import (
"golang.org/x/crypto/ssh"
"log"
"time"
)
func ConnectSSH(config *ssh.ClientConfig, serverAddrString string) (sshClientConnection *ssh.Client) {
currentRetriesServer := 0
// Loop for retries:
for {
// Try to connect to the SSH server:
if sshClientConn, err := ssh.Dial(`tcp`, serverAddrString, config); err != nil {
// Failed:
currentRetriesServer++
log.Printf("Was not able to connect with the SSH server %s: %s\n", serverAddrString, err.Error())
// Is a retry alowed?
if currentRetriesServer < maxRetriesServer {
log.Println(`Retry...`)
time.Sleep(1 * time.Second)
} else {
// After the return, this thread is closed down. The client can try it again...
log.Println(`No more retries for connecting the SSH server.`)
return
}
} else {
// Success:
log.Println(`Connected to the SSH server ` + serverAddrString)
sshClientConnection = sshClientConn
return
}
}
}
package Sync
import (
"golang.org/x/crypto/ssh"
"log"
"time"
)
func ConnectSSH(config *ssh.ClientConfig, serverAddrString string) (sshClientConnection *ssh.Client) {
currentRetriesServer := 0
// Loop for retries:
for {
// Try to connect to the SSH server:
if sshClientConn, err := ssh.Dial(`tcp`, serverAddrString, config); err != nil {
// Failed:
currentRetriesServer++
log.Printf("Was not able to connect with the SSH server %s: %s\n", serverAddrString, err.Error())
// Is a retry alowed?
if currentRetriesServer < maxRetriesServer {
log.Println(`Retry...`)
time.Sleep(1 * time.Second)
} else {
// After the return, this thread is closed down. The client can try it again...
log.Println(`No more retries for connecting the SSH server.`)
return
}
} else {
// Success:
log.Println(`Connected to the SSH server ` + serverAddrString)
sshClientConnection = sshClientConn
return
}
}
}

View File

@ -1,5 +1,5 @@
package Sync
const (
maxRetriesServer = 16 // How many retries are allowed to create the SSH server's connection?
)
package Sync
const (
maxRetriesServer = 16 // How many retries are allowed to create the SSH server's connection?
)

View File

@ -1,35 +1,35 @@
package Sync
import (
"log"
)
// Another auth. method.
func KeyboardInteractiveChallenge(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
// Log all the provided data:
log.Println(`User: ` + user)
log.Println(`Instruction: ` + instruction)
log.Println(`Questions:`)
for q := range questions {
log.Println(q)
}
// How many questions are asked?
countQuestions := len(questions)
if countQuestions == 1 {
// We expect that in this case (only one question is asked), that the server want to know the password ;-)
answers = make([]string, countQuestions, countQuestions)
answers[0] = callbackPassword
} else if countQuestions > 1 {
// After logging, this call will exit the whole program:
log.Fatalln(`The SSH server is asking multiple questions! This program cannot handle this case.`)
}
err = nil
return
}
package Sync
import (
"log"
)
// Another auth. method.
func KeyboardInteractiveChallenge(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
// Log all the provided data:
log.Println(`User: ` + user)
log.Println(`Instruction: ` + instruction)
log.Println(`Questions:`)
for q := range questions {
log.Println(q)
}
// How many questions are asked?
countQuestions := len(questions)
if countQuestions == 1 {
// We expect that in this case (only one question is asked), that the server want to know the password ;-)
answers = make([]string, countQuestions, countQuestions)
answers[0] = callbackPassword
} else if countQuestions > 1 {
// After logging, this call will exit the whole program:
log.Fatalln(`The SSH server is asking multiple questions! This program cannot handle this case.`)
}
err = nil
return
}

View File

@ -1,12 +1,18 @@
package Sync
import (
"path/filepath"
"strings"
)
func normalisePath(base, path string) string {
result := strings.Replace(path, base, ``, 1)
result = filepath.ToSlash(result)
return strings.ToLower(result)
}
package Sync
import (
"path/filepath"
"strings"
)
func normalisePath(base, path string) string {
result := strings.Replace(path, base, ``, 1)
// Ensure that the path starts with a slash:
if len(result) > 1 && result[0:1] != `/` {
result = filepath.Join(`/`, result)
}
result = filepath.ToSlash(result)
return strings.ToLower(result)
}

View File

@ -1,10 +1,10 @@
package Sync
// Just a callback function for the password request.
func PasswordCallback() (string, error) {
return callbackPassword, nil
}
func SetPassword4Callback(password string) {
callbackPassword = password
}
package Sync
// Just a callback function for the password request.
func PasswordCallback() (string, error) {
return callbackPassword, nil
}
func SetPassword4Callback(password string) {
callbackPassword = password
}

View File

@ -1,23 +1,23 @@
package Sync
import (
"fmt"
"strings"
)
func readYesNoAnswer(defaultAnswer bool) bool { // true := yes
answer := ``
if _, scanError := fmt.Scan(&answer); scanError != nil {
return defaultAnswer
}
if answer == `` || answer == ` ` {
return defaultAnswer
}
if strings.ToLower(answer) == `y` {
return true
}
return false
}
package Sync
import (
"fmt"
"strings"
)
func readYesNoAnswer(defaultAnswer bool) bool { // true := yes
answer := ``
if _, scanError := fmt.Scan(&answer); scanError != nil {
return defaultAnswer
}
if answer == `` || answer == ` ` {
return defaultAnswer
}
if strings.ToLower(answer) == `y` {
return true
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
package Sync
import (
"os"
)
var (
callbackPassword = ``
localFiles = make(map[string]os.FileInfo) // The local files with local separator
localFilesNormalised = make(map[string]os.FileInfo) // The local files with std separator i.e. / and removed base dir
normalised2localFiles = make(map[string]string) // Mapping from normalised local file to local file
remoteFiles = make(map[string]os.FileInfo) // The remote files with remote separator
remoteFilesNormalised = make(map[string]os.FileInfo) // The remote files with std separator i.e. / and removed base dir
normalised2remoteFiles = make(map[string]string) // Mapping from normalised remote file to remote file
)
package Sync
import (
"os"
)
var (
callbackPassword = ``
localFiles = make(map[string]os.FileInfo) // The local files with local separator
localFilesNormalised = make(map[string]os.FileInfo) // The local files with std separator i.e. / and removed base dir
normalised2localFiles = make(map[string]string) // Mapping from normalised local file to local file
remoteFiles = make(map[string]os.FileInfo) // The remote files with remote separator
remoteFilesNormalised = make(map[string]os.FileInfo) // The remote files with std separator i.e. / and removed base dir
normalised2remoteFiles = make(map[string]string) // Mapping from normalised remote file to remote file
)

View File

@ -1,10 +1,10 @@
package Sync
import (
"os"
)
func walkerlocal(path string, info os.FileInfo, err error) error {
localFiles[path] = info
return nil
}
package Sync
import (
"os"
)
func walkerlocal(path string, info os.FileInfo, err error) error {
localFiles[path] = info
return nil
}

View File

@ -1,11 +1,11 @@
package main
var (
username = `` // The SSH user's name
password = `` // The user's password
serverAddrString = `` // The SSH server address
localDir = `` // The local directory
remoteDir = `` // The remote directory
supervised = true // Should the tool work supervised?
pushOnly = true // Pushes only local changes to the remote i.e. backup mode
)
package main
var (
username = `` // The SSH user's name
password = `` // The user's password
serverAddrString = `` // The SSH server address
localDir = `` // The local directory
remoteDir = `` // The remote directory
supervised = true // Should the tool work supervised?
pushOnly = true // Pushes only local changes to the remote i.e. backup mode
)