Compare commits
10 commits
5eab61d350
...
2371ed3318
Author | SHA1 | Date | |
---|---|---|---|
2371ed3318 | |||
c089eb84bd | |||
9c8fe4ba1e | |||
e9f89da36f | |||
0804fadbb5 | |||
7bdce49c72 | |||
bf8eadffaf | |||
b86601cccb | |||
53f077adcb | |||
f67203286c |
9 changed files with 305 additions and 117 deletions
27
.vscode/launch.json
vendored
27
.vscode/launch.json
vendored
|
@ -8,12 +8,33 @@
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/main.go",
|
"program": "${workspaceFolder}/main.go",
|
||||||
"args": [
|
"args": [
|
||||||
"/home/velvettear/downloads/music",
|
"--verbose",
|
||||||
"192.168.1.11:/tmp",
|
|
||||||
"--password",
|
"--password",
|
||||||
"$Velvet90",
|
"$Velvet90",
|
||||||
"--concurrency",
|
"--concurrency",
|
||||||
"4",
|
"24",
|
||||||
|
"--delay",
|
||||||
|
"100",
|
||||||
|
"/mnt/kingston/downloads/music/*",
|
||||||
|
"192.168.1.11:/share/tmp/music",
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gosync-remote",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/main.go",
|
||||||
|
"args": [
|
||||||
|
"192.168.1.11:/share/music/Fu Manchu/*",
|
||||||
|
"/tmp",
|
||||||
|
"--password",
|
||||||
|
"$Velvet90",
|
||||||
|
"--concurrency",
|
||||||
|
"12",
|
||||||
|
"--delay",
|
||||||
|
"250",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal"
|
"console": "integratedTerminal"
|
||||||
|
|
19
README.md
19
README.md
|
@ -14,9 +14,16 @@ a simple wrapper for concurrent rsync processes written in golang
|
||||||
|
|
||||||
### options
|
### options
|
||||||
|
|
||||||
| short | long | description |
|
| short | long | description | default |
|
||||||
| ----- | ------------- | ------------------------------------------- |
|
| ----- | ------------- | ----------------------------------------------- | ------------------- |
|
||||||
| -u | --user | set user for ssh / rsync |
|
| -u | --user | set user for ssh / rsync | |
|
||||||
| -p | --password | set password for ssh / rsync |
|
| -p | --password | set password for ssh / rsync | |
|
||||||
| -c | --concurrency | set limit for concurrent rsync processes |
|
| -c | --concurrency | set limit for concurrent rsync processes | number of cpu cores |
|
||||||
| -v | --verbose | enable verbose / debug output |
|
| -d | --delay | set the delay between rsync connections (in ms) | 100 |
|
||||||
|
| -v | --verbose | enable verbose / debug output | |
|
||||||
|
|
||||||
|
### troubleshooting
|
||||||
|
|
||||||
|
**make sure to wrap your `[source]` and `[target]` in `"` to avoid problems with paths and / or globbing.**
|
||||||
|
|
||||||
|
if you experience errors like `kex_exchange_identification: read: Connection reset by peer` it may be helpful to increase the default delay (100ms) between rsync connections or decrease the concurrency.
|
|
@ -14,7 +14,7 @@ const LEVEL_WARNING = 2
|
||||||
const LEVEL_ERROR = 3
|
const LEVEL_ERROR = 3
|
||||||
const LEVEL_FATAL = 4
|
const LEVEL_FATAL = 4
|
||||||
|
|
||||||
var logLevel = 0
|
var logLevel = 1
|
||||||
|
|
||||||
// exported functions
|
// exported functions
|
||||||
func SetLogLevel(level int) {
|
func SetLogLevel(level int) {
|
||||||
|
@ -73,7 +73,6 @@ func trace(level int, timestamp int64, message string, extras ...string) {
|
||||||
message += " (" + suffix + ")"
|
message += " (" + suffix + ")"
|
||||||
}
|
}
|
||||||
if timestamp >= 0 {
|
if timestamp >= 0 {
|
||||||
|
|
||||||
message += " [" + strconv.Itoa(int(time.Now().UnixMilli()-timestamp)) + "ms" + "]"
|
message += " [" + strconv.Itoa(int(time.Now().UnixMilli()-timestamp)) + "ms" + "]"
|
||||||
}
|
}
|
||||||
fmt.Println(buildLogMessage(getPrefixForLogLevel(level), message))
|
fmt.Println(buildLogMessage(getPrefixForLogLevel(level), message))
|
||||||
|
|
2
main.go
2
main.go
|
@ -8,6 +8,8 @@ import (
|
||||||
"velvettear/gosync/tools"
|
"velvettear/gosync/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// version := "0.1"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
timestamp := time.Now()
|
timestamp := time.Now()
|
||||||
settings.Initialize()
|
settings.Initialize()
|
||||||
|
|
|
@ -5,82 +5,93 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"velvettear/gosync/help"
|
"velvettear/gosync/help"
|
||||||
"velvettear/gosync/log"
|
"velvettear/gosync/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// exported function(s)
|
// exported function(s)
|
||||||
func Initialize() {
|
func Initialize() {
|
||||||
os.Args = os.Args[1:]
|
|
||||||
var arguments []string
|
var arguments []string
|
||||||
for index, arg := range os.Args {
|
for index := 1; index < len(os.Args); index++ {
|
||||||
switch strings.ToLower(arg) {
|
arg := strings.ToLower(os.Args[index])
|
||||||
|
if arg != "-v" && arg != "--verbose" {
|
||||||
|
arguments = append(arguments, os.Args[index])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
setVerbose(true)
|
||||||
|
}
|
||||||
|
var filteredArguments []string
|
||||||
|
for index := 0; index < len(arguments); index++ {
|
||||||
|
switch strings.ToLower(arguments[index]) {
|
||||||
case "-h":
|
case "-h":
|
||||||
fallthrough
|
fallthrough
|
||||||
case "--help":
|
case "--help":
|
||||||
help.Print()
|
help.Print()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
case "-v":
|
|
||||||
fallthrough
|
|
||||||
case "--verbose":
|
|
||||||
setVerbose(true)
|
|
||||||
case "-c":
|
case "-c":
|
||||||
fallthrough
|
fallthrough
|
||||||
case "--concurrency":
|
case "--concurrency":
|
||||||
var concurrency int
|
index++
|
||||||
tmpIndex := index + 1
|
if index < len(arguments) {
|
||||||
if tmpIndex < len(os.Args) {
|
concurrency, error := strconv.Atoi(arguments[index])
|
||||||
tmp, error := strconv.Atoi(os.Args[tmpIndex])
|
if error != nil {
|
||||||
if error == nil {
|
break
|
||||||
concurrency = tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if concurrency == 0 {
|
|
||||||
concurrency = runtime.NumCPU()
|
|
||||||
}
|
}
|
||||||
setConcurrency(concurrency)
|
setConcurrency(concurrency)
|
||||||
|
}
|
||||||
|
case "-d":
|
||||||
|
fallthrough
|
||||||
|
case "--delay":
|
||||||
|
index++
|
||||||
|
if index < len(arguments) {
|
||||||
|
delay, error := strconv.Atoi(arguments[index])
|
||||||
|
if error != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setDelay(time.Duration(delay) * time.Millisecond)
|
||||||
|
}
|
||||||
case "-p":
|
case "-p":
|
||||||
fallthrough
|
fallthrough
|
||||||
case "--password":
|
case "--password":
|
||||||
tmpIndex := index + 1
|
index++
|
||||||
if index > len(os.Args) {
|
if index > len(arguments) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
setPassword(os.Args[tmpIndex])
|
setPassword(arguments[index])
|
||||||
case "-u":
|
case "-u":
|
||||||
fallthrough
|
fallthrough
|
||||||
case "--user":
|
case "--user":
|
||||||
tmpIndex := index + 1
|
index++
|
||||||
if index > len(os.Args) {
|
if index > len(arguments) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
setUser(os.Args[tmpIndex])
|
setUser(arguments[index])
|
||||||
default:
|
default:
|
||||||
arguments = append(arguments, arg)
|
filteredArguments = append(filteredArguments, arguments[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(os.Args) < 2 {
|
if len(arguments) < 2 {
|
||||||
log.Fatal("error: missing arguments")
|
log.Fatal("error: missing arguments")
|
||||||
}
|
}
|
||||||
setSource(arguments[0])
|
setSource(filteredArguments[0])
|
||||||
setTarget(arguments[1])
|
setTarget(filteredArguments[1])
|
||||||
_, error := os.Stat(Source)
|
if !SourceIsRemote() {
|
||||||
|
source, _ := strings.CutSuffix(Source, "/*")
|
||||||
|
_, error := os.Stat(source)
|
||||||
if os.IsNotExist(error) {
|
if os.IsNotExist(error) {
|
||||||
log.Fatal("given source does not exist", Source)
|
log.Fatal("given source does not exist", source)
|
||||||
}
|
}
|
||||||
if !Verbose {
|
|
||||||
setVerbose(false)
|
|
||||||
}
|
}
|
||||||
|
setDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
// unexported function(s)
|
// unexported function(s)
|
||||||
func removeArgument(index int) {
|
func setDefaults() {
|
||||||
removeArguments(index, 0, 0)
|
if Concurrency == 0 {
|
||||||
}
|
setConcurrency(runtime.NumCPU())
|
||||||
|
}
|
||||||
func removeArguments(index int, before int, after int) {
|
if Delay == 0 {
|
||||||
// derp := index - 1 - before
|
setDelay(100)
|
||||||
copyArgs := os.Args[0 : index-before]
|
}
|
||||||
copyArgs = append(copyArgs, os.Args[index+1+after:]...)
|
|
||||||
os.Args = copyArgs
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package settings
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"velvettear/gosync/log"
|
"velvettear/gosync/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,15 +12,28 @@ var Verbose bool
|
||||||
var Source string
|
var Source string
|
||||||
var Target string
|
var Target string
|
||||||
var Concurrency int
|
var Concurrency int
|
||||||
|
var Delay time.Duration
|
||||||
var Password string
|
var Password string
|
||||||
var User string
|
var User string
|
||||||
|
|
||||||
// exported function(s)
|
// exported function(s)
|
||||||
func TargetIsRemote() bool {
|
func TargetIsRemote() bool {
|
||||||
return strings.Contains(Target, ":")
|
return isRemote(Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SourceIsRemote() bool {
|
||||||
|
return isRemote(Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SourceIsWildcard() bool {
|
||||||
|
return strings.HasSuffix(Source, "/*")
|
||||||
}
|
}
|
||||||
|
|
||||||
// unexported function(s)
|
// unexported function(s)
|
||||||
|
func isRemote(target string) bool {
|
||||||
|
return strings.Contains(target, ":")
|
||||||
|
}
|
||||||
|
|
||||||
func setVerbose(verbose bool) {
|
func setVerbose(verbose bool) {
|
||||||
Verbose = verbose
|
Verbose = verbose
|
||||||
if Verbose {
|
if Verbose {
|
||||||
|
@ -45,6 +59,13 @@ func setConcurrency(concurrency int) {
|
||||||
log.Debug("set concurrency", strconv.Itoa(Concurrency))
|
log.Debug("set concurrency", strconv.Itoa(Concurrency))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDelay(delay time.Duration) {
|
||||||
|
Delay = delay
|
||||||
|
ms := Delay.Milliseconds()
|
||||||
|
derp := strconv.FormatInt(ms, 10)
|
||||||
|
log.Debug("set delay", derp)
|
||||||
|
}
|
||||||
|
|
||||||
func setPassword(password string) {
|
func setPassword(password string) {
|
||||||
Password = password
|
Password = password
|
||||||
log.Debug("set password", Password)
|
log.Debug("set password", Password)
|
||||||
|
|
139
tools/rsync.go
139
tools/rsync.go
|
@ -1,6 +1,7 @@
|
||||||
package tools
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -17,83 +18,102 @@ import (
|
||||||
"github.com/vbauerster/mpb/v8/decor"
|
"github.com/vbauerster/mpb/v8/decor"
|
||||||
)
|
)
|
||||||
|
|
||||||
var transferSize float64
|
var transferSize int64
|
||||||
|
|
||||||
|
const byteToMegabyte = 1048576
|
||||||
|
const megabyteToGigabyte = 1024
|
||||||
|
|
||||||
// exported function(s)
|
// exported function(s)
|
||||||
func Transfer() {
|
func Transfer() {
|
||||||
transferSize = 0
|
|
||||||
sourcefiles := getSourceFiles()
|
sourcefiles := getSourceFiles()
|
||||||
sourcefilesCount := len(sourcefiles)
|
sourcefilesCount := len(sourcefiles)
|
||||||
|
if sourcefilesCount <= 0 {
|
||||||
|
log.Info("nothing to do - exiting...")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
sourceFileSize, sourceFileSizeName := humanizeBytes(getSourceFileSize())
|
||||||
var waitgroup sync.WaitGroup
|
var waitgroup sync.WaitGroup
|
||||||
waitgroup.Add(sourcefilesCount)
|
waitgroup.Add(sourcefilesCount)
|
||||||
barcontainer := mpb.New(
|
barcontainer := mpb.New(
|
||||||
mpb.WithWaitGroup(&waitgroup),
|
mpb.WithWaitGroup(&waitgroup),
|
||||||
)
|
)
|
||||||
timestamp := time.Now()
|
timestamp := time.Now()
|
||||||
counter := 0
|
totalbar := createProgressBar(barcontainer, strconv.FormatFloat(sourceFileSize, 'f', 2, 64)+sourceFileSizeName+" Total", int64(0), int64(sourcefilesCount), sourcefilesCount+1, true)
|
||||||
totalbar := createProgressBar(barcontainer, "Total", int64(0), int64(sourcefilesCount), sourcefilesCount+1, true)
|
|
||||||
concurrency := settings.Concurrency
|
concurrency := settings.Concurrency
|
||||||
if concurrency == 0 {
|
if concurrency == 0 {
|
||||||
concurrency = sourcefilesCount
|
concurrency = sourcefilesCount
|
||||||
}
|
}
|
||||||
|
counter := 0
|
||||||
|
index := 0
|
||||||
|
errors := make(map[string]error)
|
||||||
channel := make(chan struct{}, concurrency)
|
channel := make(chan struct{}, concurrency)
|
||||||
for index, file := range sourcefiles {
|
for file, size := range sourcefiles {
|
||||||
channel <- struct{}{}
|
channel <- struct{}{}
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(settings.Delay)
|
||||||
}
|
}
|
||||||
go func(index int, file string) {
|
go func(file string, size int64, index int) {
|
||||||
defer waitgroup.Done()
|
defer waitgroup.Done()
|
||||||
stats, error := os.Stat(file)
|
bar := createProgressBar(barcontainer, filepath.Base(file), size, int64(100), index, false)
|
||||||
|
error := transferFile(bar, file)
|
||||||
if error != nil {
|
if error != nil {
|
||||||
log.Warning("encountered an error getting file size", error.Error())
|
errors[file] = error
|
||||||
return
|
bar.Abort(false)
|
||||||
}
|
bar.Wait()
|
||||||
bar := createProgressBar(barcontainer, filepath.Base(file), stats.Size(), int64(100), index, false)
|
} else {
|
||||||
if transferFile(bar, file) {
|
|
||||||
counter++
|
counter++
|
||||||
}
|
}
|
||||||
totalbar.Increment()
|
totalbar.Increment()
|
||||||
<-channel
|
<-channel
|
||||||
}(index, file)
|
}(file, size, index)
|
||||||
|
index++
|
||||||
}
|
}
|
||||||
barcontainer.Wait()
|
barcontainer.Wait()
|
||||||
timeDifference := time.Since(timestamp)
|
timeDifference := time.Since(timestamp)
|
||||||
transferSpeedName := "bytes/sec"
|
transferSpeedName := "B/sec"
|
||||||
transferSpeed := transferSize / timeDifference.Seconds()
|
transferSpeed := float64(transferSize) / timeDifference.Seconds()
|
||||||
if transferSpeed > 1048576 {
|
transferSpeed, transferSpeedName = humanizeBytes(int64(transferSpeed))
|
||||||
transferSpeed = transferSpeed / 1048576
|
transferSpeedName += "/s"
|
||||||
transferSpeedName = "mb/s"
|
transferSize, transferSizeName := humanizeBytes(transferSize)
|
||||||
}
|
|
||||||
transferSizeName := "bytes"
|
|
||||||
if transferSize > 1048576 {
|
|
||||||
transferSize = transferSize / 1048576
|
|
||||||
transferSizeName = "mb"
|
|
||||||
}
|
|
||||||
log.InfoTimed("transferred "+strconv.Itoa(counter)+" files, "+strconv.Itoa(int(transferSize))+" "+transferSizeName+" ("+strconv.FormatFloat(transferSpeed, 'f', 2, 64)+" "+transferSpeedName+")", timestamp.UnixMilli())
|
log.InfoTimed("transferred "+strconv.Itoa(counter)+" files, "+strconv.Itoa(int(transferSize))+" "+transferSizeName+" ("+strconv.FormatFloat(transferSpeed, 'f', 2, 64)+" "+transferSpeedName+")", timestamp.UnixMilli())
|
||||||
|
if len(errors) > 0 {
|
||||||
|
log.Info("encountered " + strconv.Itoa(len(errors)) + " errors")
|
||||||
|
for file, error := range errors {
|
||||||
|
log.Info(file, error.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// unexported function(s)
|
// unexported function(s)
|
||||||
func transferFile(bar *mpb.Bar, file string) bool {
|
func transferFile(bar *mpb.Bar, file string) error {
|
||||||
target := getTargetLocation(file)
|
target := getTargetLocation(file)
|
||||||
var arguments []string
|
var arguments []string
|
||||||
if len(settings.Password) > 0 {
|
if len(settings.Password) > 0 {
|
||||||
arguments = append(arguments, "-p", settings.Password)
|
arguments = append(arguments, "-p", settings.Password)
|
||||||
}
|
}
|
||||||
arguments = append(arguments, "rsync", "-avz", "--mkpath", file)
|
if settings.SourceIsRemote() {
|
||||||
|
remote, _, _ := strings.Cut(settings.Source, ":")
|
||||||
|
file = remote + ":" + file
|
||||||
|
if len(settings.User) > 0 {
|
||||||
|
file = settings.User + "@" + file
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if len(settings.User) > 0 {
|
if len(settings.User) > 0 {
|
||||||
target = settings.User + "@" + target
|
target = settings.User + "@" + target
|
||||||
}
|
}
|
||||||
arguments = append(arguments, target, "--progress")
|
}
|
||||||
|
arguments = append(arguments, "rsync", "-avz", "--secluded-args", "--mkpath", file, target, "--progress")
|
||||||
cmd := exec.Command("sshpass", arguments...)
|
cmd := exec.Command("sshpass", arguments...)
|
||||||
stdout, stdoutError := cmd.StdoutPipe()
|
stdout, stdoutError := cmd.StdoutPipe()
|
||||||
stderr, stderrError := cmd.StderrPipe()
|
stderr, stderrError := cmd.StderrPipe()
|
||||||
cmd.Start()
|
cmd.Start()
|
||||||
if stdoutError != nil {
|
if stdoutError != nil {
|
||||||
log.Fatal(stdoutError.Error())
|
log.Warning("encountered an error opening remote stdout pipe", "file: "+file, stdoutError.Error())
|
||||||
|
return stderrError
|
||||||
}
|
}
|
||||||
if stderrError != nil {
|
if stderrError != nil {
|
||||||
log.Fatal(stderrError.Error())
|
log.Warning("encountered an error opening remote stderr pipe", "file: "+file, stdoutError.Error())
|
||||||
|
return stderrError
|
||||||
}
|
}
|
||||||
var resultBytes []byte
|
var resultBytes []byte
|
||||||
var waitgroup sync.WaitGroup
|
var waitgroup sync.WaitGroup
|
||||||
|
@ -105,7 +125,7 @@ func transferFile(bar *mpb.Bar, file string) bool {
|
||||||
readBytes, error := stdout.Read(tmp)
|
readBytes, error := stdout.Read(tmp)
|
||||||
if error != nil {
|
if error != nil {
|
||||||
if error != io.EOF {
|
if error != io.EOF {
|
||||||
log.Warning("encountered an error reading stdout", error.Error())
|
log.Warning("encountered an error reading stdout", "file: "+file, error.Error())
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -119,9 +139,9 @@ func transferFile(bar *mpb.Bar, file string) bool {
|
||||||
_, tmp, _ := strings.Cut(lowerline, "sent")
|
_, tmp, _ := strings.Cut(lowerline, "sent")
|
||||||
tmp, _, _ = strings.Cut(tmp, "bytes")
|
tmp, _, _ = strings.Cut(tmp, "bytes")
|
||||||
tmp = strings.ReplaceAll(strings.TrimSpace(tmp), ".", "")
|
tmp = strings.ReplaceAll(strings.TrimSpace(tmp), ".", "")
|
||||||
bytes, error := strconv.ParseFloat(tmp, 64)
|
bytes, error := strconv.ParseInt(tmp, 10, 64)
|
||||||
if error != nil {
|
if error != nil {
|
||||||
log.Fatal("encountered an error converting the transferred bytes to int", error.Error())
|
log.Fatal("encountered an error converting the transferred bytes to int", "file: "+file, error.Error())
|
||||||
}
|
}
|
||||||
transferSize += bytes
|
transferSize += bytes
|
||||||
}
|
}
|
||||||
|
@ -151,46 +171,65 @@ func transferFile(bar *mpb.Bar, file string) bool {
|
||||||
}()
|
}()
|
||||||
errorBytes, stderrError := io.ReadAll(stderr)
|
errorBytes, stderrError := io.ReadAll(stderr)
|
||||||
if stderrError != nil {
|
if stderrError != nil {
|
||||||
log.Fatal(stderrError.Error())
|
log.Warning("encountered an error reading from remote stderr", "file: "+file, stdoutError.Error())
|
||||||
|
return stderrError
|
||||||
}
|
}
|
||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
error := strings.Trim(string(errorBytes), "\n")
|
error := strings.Trim(string(errorBytes), "\n")
|
||||||
if len(error) > 0 {
|
if len(error) > 0 {
|
||||||
log.Fatal(error)
|
log.Warning("encountered an remote error", "file: "+file, error)
|
||||||
|
return errors.New(error)
|
||||||
}
|
}
|
||||||
waitgroup.Wait()
|
waitgroup.Wait()
|
||||||
stdout.Close()
|
stdout.Close()
|
||||||
stderr.Close()
|
stderr.Close()
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTargetLocation(source string) string {
|
func getTargetLocation(sourceFile string) string {
|
||||||
if source == settings.Source {
|
if sourceFile == settings.Source {
|
||||||
return filepath.Join(settings.Target, filepath.Base(source))
|
return filepath.Join(settings.Target, filepath.Base(sourceFile))
|
||||||
}
|
}
|
||||||
return filepath.Join(settings.Target, strings.Replace(source, filepath.Dir(settings.Source), "", 1))
|
source, _ := strings.CutSuffix(settings.Source, "/*")
|
||||||
|
if settings.SourceIsRemote() {
|
||||||
|
_, source, _ = strings.Cut(source, ":")
|
||||||
|
source = filepath.Dir(source)
|
||||||
|
}
|
||||||
|
return filepath.Join(settings.Target, strings.Replace(sourceFile, source, "", 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func createProgressBar(barcontainer *mpb.Progress, name string, size int64, max int64, priority int, total bool) *mpb.Bar {
|
func createProgressBar(barcontainer *mpb.Progress, name string, size int64, max int64, priority int, total bool) *mpb.Bar {
|
||||||
red, green, magenta, yellow := color.New(color.FgRed), color.New(color.FgGreen), color.New(color.FgMagenta), color.New(color.FgYellow)
|
_, green, magenta, yellow, blue := color.New(color.FgRed), color.New(color.FgGreen), color.New(color.FgMagenta), color.New(color.FgYellow), color.New(color.FgBlue)
|
||||||
barstyle := mpb.BarStyle().Lbound("").Filler("").Tip("").Padding(" ").Rbound("")
|
barstyle := mpb.BarStyle().Lbound("").Filler("").Tip("").Padding(" ").Rbound("")
|
||||||
defaultBarPrepend := mpb.PrependDecorators(
|
defaultBarPrepend := mpb.PrependDecorators(
|
||||||
decor.Name("[info] > "),
|
decor.Name("[info] > "),
|
||||||
decor.OnCompleteMeta(
|
decor.OnCompleteMeta(
|
||||||
decor.OnComplete(
|
decor.OnComplete(
|
||||||
decor.Meta(decor.Name(name, decor.WCSyncSpaceR), colorize(red)), ""+name,
|
decor.Meta(decor.Name(name, decor.WCSyncSpaceR), colorize(blue)), ""+name,
|
||||||
),
|
),
|
||||||
colorize(green),
|
colorize(green),
|
||||||
),
|
),
|
||||||
|
// decor.OnAbortMeta(
|
||||||
|
// decor.OnAbort(
|
||||||
|
// decor.Meta(decor.Name(name, decor.WCSyncSpaceR), colorize(blue)), ""+name,
|
||||||
|
// ),
|
||||||
|
// colorize(red),
|
||||||
|
// ),
|
||||||
)
|
)
|
||||||
defaultBarAppend := mpb.AppendDecorators(
|
defaultBarAppend := mpb.AppendDecorators(
|
||||||
decor.OnCompleteMeta(decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncSpaceR), colorize(yellow)),
|
decor.OnCompleteMeta(decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncSpaceR), colorize(yellow)),
|
||||||
decor.OnComplete(
|
decor.OnComplete(
|
||||||
decor.Name("|", decor.WCSyncSpaceR), "",
|
decor.Name("", decor.WCSyncSpaceR), "",
|
||||||
),
|
),
|
||||||
decor.OnComplete(
|
decor.OnComplete(
|
||||||
decor.Percentage(decor.WCSyncSpaceR), "",
|
decor.Percentage(decor.WCSyncSpaceR), "",
|
||||||
),
|
),
|
||||||
|
// decor.OnAbort(
|
||||||
|
// decor.Name("", decor.WCSyncSpaceR), "",
|
||||||
|
// ),
|
||||||
|
// decor.OnAbort(
|
||||||
|
// decor.Percentage(decor.WCSyncSpace), "",
|
||||||
|
// ),
|
||||||
)
|
)
|
||||||
totalBarPrepend := mpb.PrependDecorators(
|
totalBarPrepend := mpb.PrependDecorators(
|
||||||
decor.Name("[info] > "),
|
decor.Name("[info] > "),
|
||||||
|
@ -229,8 +268,16 @@ func colorize(c *color.Color) func(string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func decolorize() func(string) string {
|
func humanizeBytes(bytes int64) (float64, string) {
|
||||||
return func(s string) string {
|
name := "B"
|
||||||
return ""
|
size := float64(bytes)
|
||||||
|
if size >= byteToMegabyte {
|
||||||
|
name = "MB"
|
||||||
|
size = size / byteToMegabyte
|
||||||
}
|
}
|
||||||
|
if size >= megabyteToGigabyte {
|
||||||
|
name = "GB"
|
||||||
|
size = size / megabyteToGigabyte
|
||||||
|
}
|
||||||
|
return size, name
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,97 @@
|
||||||
package tools
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"velvettear/gosync/log"
|
"velvettear/gosync/log"
|
||||||
"velvettear/gosync/settings"
|
"velvettear/gosync/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var sourceFiles []string
|
var sourceFiles map[string]int64
|
||||||
|
|
||||||
// unexported function(s)
|
// unexported function(s)
|
||||||
func getSourceFiles() []string {
|
func getSourceFiles() map[string]int64 {
|
||||||
timestamp := time.Now()
|
timestamp := time.Now()
|
||||||
stats, error := os.Stat(settings.Source)
|
sourceFiles = map[string]int64{}
|
||||||
|
if settings.SourceIsRemote() {
|
||||||
|
log.Info("getting the list of remote source files...")
|
||||||
|
error := fillRemoteSourceFiles()
|
||||||
if error != nil {
|
if error != nil {
|
||||||
log.Error("encountered an error getting the stats for the source", error.Error())
|
log.Fatal("encountered an error getting the list of remote source files", error.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source, _ := strings.CutSuffix(settings.Source, "/*")
|
||||||
|
stats, error := os.Stat(source)
|
||||||
|
if error != nil {
|
||||||
|
log.Fatal("encountered an error getting stats for source", error.Error())
|
||||||
}
|
}
|
||||||
if stats.IsDir() {
|
if stats.IsDir() {
|
||||||
log.Info("scanning source...", settings.Source)
|
log.Info("scanning source...", source)
|
||||||
filepath.WalkDir(settings.Source, fillSourceFiles)
|
filepath.WalkDir(source, fillSourceFiles)
|
||||||
log.InfoTimed("found "+strconv.Itoa(len(sourceFiles))+" source files", timestamp.UnixMilli())
|
|
||||||
} else {
|
} else {
|
||||||
sourceFiles = append(sourceFiles, settings.Source)
|
sourceFiles[settings.Source] = stats.Size()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
log.InfoTimed("found "+strconv.Itoa(len(sourceFiles))+" source files", timestamp.UnixMilli())
|
||||||
return sourceFiles
|
return sourceFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fillRemoteSourceFiles() error {
|
||||||
|
var arguments []string
|
||||||
|
if len(settings.Password) > 0 {
|
||||||
|
arguments = append(arguments, "-p", settings.Password)
|
||||||
|
}
|
||||||
|
arguments = append(arguments, "ssh")
|
||||||
|
remote, path, _ := strings.Cut(settings.Source, ":")
|
||||||
|
path, _ = strings.CutSuffix(path, "/*")
|
||||||
|
if len(settings.User) > 0 {
|
||||||
|
remote = settings.User + "@" + remote
|
||||||
|
}
|
||||||
|
arguments = append(arguments, remote, "find", "\""+path+"\"", "-type", "f", "-exec", "du", "-b", "{}", "\\;")
|
||||||
|
cmd := exec.Command("sshpass", arguments...)
|
||||||
|
stdout, stdoutError := cmd.StdoutPipe()
|
||||||
|
stderr, stderrError := cmd.StderrPipe()
|
||||||
|
cmd.Start()
|
||||||
|
if stdoutError != nil {
|
||||||
|
return stdoutError
|
||||||
|
}
|
||||||
|
if stderrError != nil {
|
||||||
|
return stderrError
|
||||||
|
}
|
||||||
|
outBytes, stdoutError := io.ReadAll(stdout)
|
||||||
|
if stdoutError != nil {
|
||||||
|
return stdoutError
|
||||||
|
}
|
||||||
|
errorBytes, stderrError := io.ReadAll(stderr)
|
||||||
|
if stderrError != nil {
|
||||||
|
return stderrError
|
||||||
|
}
|
||||||
|
cmd.Wait()
|
||||||
|
error := strings.TrimSpace(string(errorBytes))
|
||||||
|
if len(error) > 0 {
|
||||||
|
return errors.New(error)
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(outBytes), "\n") {
|
||||||
|
if !strings.Contains(line, "\t") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Split(line, "\t")
|
||||||
|
size, error := strconv.ParseInt(parts[0], 10, 64)
|
||||||
|
if error != nil {
|
||||||
|
log.Warning("encountered an error getting the file size for file '"+path+"'", error.Error())
|
||||||
|
}
|
||||||
|
sourceFiles[parts[1]] = size
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func fillSourceFiles(path string, dir fs.DirEntry, err error) error {
|
func fillSourceFiles(path string, dir fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -36,6 +99,19 @@ func fillSourceFiles(path string, dir fs.DirEntry, err error) error {
|
||||||
if dir.IsDir() {
|
if dir.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sourceFiles = append(sourceFiles, path)
|
stats, error := os.Stat(path)
|
||||||
|
if error != nil {
|
||||||
|
log.Fatal("encountered an error getting stats for file", error.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sourceFiles[path] = stats.Size()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSourceFileSize() int64 {
|
||||||
|
var total int64
|
||||||
|
for _, size := range sourceFiles {
|
||||||
|
total += size
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
12
tools/ssh.go
12
tools/ssh.go
|
@ -11,7 +11,12 @@ import (
|
||||||
|
|
||||||
// exported function(s)
|
// exported function(s)
|
||||||
func TestConnection() error {
|
func TestConnection() error {
|
||||||
if !settings.TargetIsRemote() {
|
var remote string
|
||||||
|
if settings.SourceIsRemote() {
|
||||||
|
remote, _, _ = strings.Cut(settings.Source, ":")
|
||||||
|
} else if settings.TargetIsRemote() {
|
||||||
|
remote, _, _ = strings.Cut(settings.Target, ":")
|
||||||
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var arguments []string
|
var arguments []string
|
||||||
|
@ -19,11 +24,10 @@ func TestConnection() error {
|
||||||
arguments = append(arguments, "-p", settings.Password)
|
arguments = append(arguments, "-p", settings.Password)
|
||||||
}
|
}
|
||||||
arguments = append(arguments, "ssh")
|
arguments = append(arguments, "ssh")
|
||||||
target, _, _ := strings.Cut(settings.Target, ":")
|
|
||||||
if len(settings.User) > 0 {
|
if len(settings.User) > 0 {
|
||||||
target = settings.User + "@" + target
|
remote = settings.User + "@" + remote
|
||||||
}
|
}
|
||||||
arguments = append(arguments, target, "'exit'")
|
arguments = append(arguments, remote, "exit")
|
||||||
cmd := exec.Command("sshpass", arguments...)
|
cmd := exec.Command("sshpass", arguments...)
|
||||||
stdout, stdoutError := cmd.StdoutPipe()
|
stdout, stdoutError := cmd.StdoutPipe()
|
||||||
stderr, stderrError := cmd.StderrPipe()
|
stderr, stderrError := cmd.StderrPipe()
|
||||||
|
|
Loading…
Reference in a new issue