mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-28 21:06:42 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d35c9c2186 | ||
|
|
d70bd62372 | ||
|
|
e1222f94d9 | ||
|
|
e9d0c37043 | ||
|
|
173574eb85 | ||
|
|
4689cae3f4 | ||
|
|
eb2b349697 | ||
|
|
038e386df7 | ||
|
|
fc1cd349b8 | ||
|
|
fb5449cf1f | ||
|
|
55be8a3075 | ||
|
|
c71cacb507 | ||
|
|
81b78068a7 | ||
|
|
89b74cec1e | ||
|
|
0162b737c8 | ||
|
|
1c650d4751 | ||
|
|
5028fb21b0 | ||
|
|
4ca97834f4 | ||
|
|
b37dfdb912 | ||
|
|
cdb2318dc1 | ||
|
|
68e3108741 | ||
|
|
b5dd8ec502 | ||
|
|
d2034b458b | ||
|
|
fc2e77d8b8 | ||
|
|
c28d45c9da | ||
|
|
2feeeb74d7 | ||
|
|
1a1d4ed1e4 | ||
|
|
a4a39a3a99 | ||
|
|
1b9237af0b | ||
|
|
049af83390 | ||
|
|
f9aa2fc374 | ||
|
|
3b57d59ddc | ||
|
|
9ed7b8ad9b | ||
|
|
a4864b6544 | ||
|
|
619e4220a6 | ||
|
|
f2f9c523f4 | ||
|
|
1cebb39546 |
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 Randall Winkhart
|
Copyright (c) 2024-2025 Randall Winkhart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ libmutton is a library for building simple, SSH-synchronized password managers i
|
|||||||
>I am not responsible for any data loss or breaches of your information resulting from the use of libmutton.
|
>I am not responsible for any data loss or breaches of your information resulting from the use of libmutton.
|
||||||
>libmutton is a new project that is constantly being updated, and though safety and security are priorities, they cannot be guaranteed.
|
>libmutton is a new project that is constantly being updated, and though safety and security are priorities, they cannot be guaranteed.
|
||||||
|
|
||||||
# Developing Thid-Party Clients
|
# Developing Third-Party Clients
|
||||||
See the [developer guide](https://github.com/rwinkhart/libmutton/blob/main/wiki/developers.md).
|
See the [developer guide](https://github.com/rwinkhart/libmutton/blob/main/wiki/developers.md).
|
||||||
|
|
||||||
# Roadmap
|
# Roadmap
|
||||||
|
|||||||
+8
-5
@@ -4,16 +4,19 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ByteInputFetcher func(prompt string) []byte
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Home, _ = os.UserHomeDir()
|
PassphraseInputFunction ByteInputFetcher // Clients should set this to a function that fetches hidden input from the user
|
||||||
|
Home, _ = os.UserHomeDir()
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LibmuttonVersion = "0.2.4" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.2.A" -> "0.2.0", "0.2.B" -> "0.2.1"
|
LibmuttonVersion = "0.3.0" // Untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.2.A" -> "0.2.0", "0.2.B" -> "0.2.1"
|
||||||
|
|
||||||
FSSpace = "\u259d" // ▝ space/list separator
|
FSSpace = "\u259d" // ▝ Space/list separator
|
||||||
FSPath = "\u259e" // ▞ path separator
|
FSPath = "\u259e" // ▞ Path separator
|
||||||
FSMisc = "\u259f" // ▟ misc. field separator (if \u259d is already used)
|
FSMisc = "\u259f" // ▟ Misc. field separator (if \u259d is already used)
|
||||||
|
|
||||||
AnsiError = "\033[38;5;9m"
|
AnsiError = "\033[38;5;9m"
|
||||||
AnsiReset = "\033[0m"
|
AnsiReset = "\033[0m"
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
var EntryRoot = Home + "/.local/share/libmutton" // path to libmutton entry directory
|
var EntryRoot = Home + "/.local/share/libmutton" // Path to libmutton entry directory
|
||||||
var ConfigDir = Home + "/.config/libmutton" // path to libmutton configuration directory
|
var ConfigDir = Home + "/.config/libmutton" // Path to libmutton configuration directory
|
||||||
var ConfigPath = ConfigDir + "/libmutton.ini" // path to libmutton configuration file
|
var ConfigPath = ConfigDir + "/libmutton.ini" // Path to libmutton configuration file
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PathSeparator = "/" // platform-specific path separator
|
PathSeparator = "/" // Platform-specific path separator
|
||||||
IsWindows = false // platform indicator
|
IsWindows = false // Platform indicator
|
||||||
)
|
)
|
||||||
|
|
||||||
// enableVirtualTerminalProcessing is a dummy function on UNIX-like systems (only needed on Windows).
|
// enableVirtualTerminalProcessing is a dummy function on UNIX-like systems (only needed on Windows).
|
||||||
|
|||||||
+5
-5
@@ -7,13 +7,13 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
var EntryRoot = Home + "\\AppData\\Local\\libmutton\\entries" // path to libmutton entry directory
|
var EntryRoot = Home + "\\AppData\\Local\\libmutton\\entries" // Path to libmutton entry directory
|
||||||
var ConfigDir = Home + "\\AppData\\Local\\libmutton\\config" // path to libmutton configuration directory
|
var ConfigDir = Home + "\\AppData\\Local\\libmutton\\config" // Path to libmutton configuration directory
|
||||||
var ConfigPath = ConfigDir + "\\libmutton.ini" // path to libmutton configuration file
|
var ConfigPath = ConfigDir + "\\libmutton.ini" // Path to libmutton configuration file
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PathSeparator = "\\" // platform-specific path separator
|
PathSeparator = "\\" // Platform-specific path separator
|
||||||
IsWindows = true // platform indicator
|
IsWindows = true // Platform indicator
|
||||||
)
|
)
|
||||||
|
|
||||||
// enableVirtualTerminalProcessing ensures ANSI escape sequences are interpreted properly on Windows.
|
// enableVirtualTerminalProcessing ensures ANSI escape sequences are interpreted properly on Windows.
|
||||||
|
|||||||
+27
-16
@@ -13,17 +13,18 @@ import (
|
|||||||
func loadConfig() *ini.File {
|
func loadConfig() *ini.File {
|
||||||
cfg, err := ini.Load(ConfigPath)
|
cfg, err := ini.Load(ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to load libmutton.ini:", err.Error()+AnsiReset)
|
PrintError("Failed to load libmutton.ini: "+err.Error(), ErrorRead, true)
|
||||||
os.Exit(ErrorRead)
|
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseConfig reads the libmutton.ini file and returns a slice of values for the specified keys.
|
// ParseConfig reads the libmutton.ini file and returns a slice of values for the specified keys.
|
||||||
// Requires: requestedValues (a slice of length 2 arrays each containing a section and a key name),
|
// Requires: valuesRequested (a slice of length 2 arrays each containing a section and a key name),
|
||||||
// missingValueError (an error message to display if a key is missing a value, set to "" for auto-generated or "0" to exit/return silently with code 0).
|
// missingValueError (an error message to display if a key is missing a value, set to "" for auto-generated or "0" to exit/return silently with code 0).
|
||||||
// Returns: config (slice of values for the specified keys).
|
// Returns: config (slice of values for the specified keys),
|
||||||
func ParseConfig(valuesRequested [][2]string, missingValueError string) []string {
|
// error (nil if no error occurred, otherwise an error using the generated or provided message).
|
||||||
|
func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]string, error) {
|
||||||
|
var err error
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
|
|
||||||
var config []string
|
var config []string
|
||||||
@@ -35,19 +36,21 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) []string
|
|||||||
if value == "" {
|
if value == "" {
|
||||||
switch missingValueError {
|
switch missingValueError {
|
||||||
case "":
|
case "":
|
||||||
fmt.Println(AnsiError + "Failed to find value for key \"" + pair[1] + "\" in section \"[" + pair[0] + "]\" in libmutton.ini" + AnsiReset)
|
err = fmt.Errorf("Failed to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
|
||||||
case "0":
|
case "0":
|
||||||
Exit(0)
|
Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
|
||||||
default:
|
default:
|
||||||
fmt.Println(AnsiError + missingValueError + AnsiReset)
|
err = fmt.Errorf("%s", missingValueError)
|
||||||
}
|
}
|
||||||
os.Exit(ErrorRead)
|
PrintError(err.Error(), ErrorRead, false)
|
||||||
|
// if interactive (soft exit), return nil and the error to be handled by the caller
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config = append(config, value)
|
config = append(config, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenDeviceIDList returns a pointer to a slice of all registered device IDs.
|
// GenDeviceIDList returns a pointer to a slice of all registered device IDs.
|
||||||
@@ -57,8 +60,7 @@ func GenDeviceIDList(errorOnFail bool) *[]fs.DirEntry {
|
|||||||
deviceIDList, err := os.ReadDir(ConfigDir + PathSeparator + "devices")
|
deviceIDList, err := os.ReadDir(ConfigDir + PathSeparator + "devices")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errorOnFail {
|
if errorOnFail {
|
||||||
fmt.Println(AnsiError+"Failed to read the devices directory:", err.Error()+AnsiReset)
|
PrintError("Failed to read the devices directory: "+err.Error(), ErrorRead, true)
|
||||||
os.Exit(ErrorRead)
|
|
||||||
} else {
|
} else {
|
||||||
return nil // a nil return value indicates that the devices directory could not be read/does not exist
|
return nil // a nil return value indicates that the devices directory could not be read/does not exist
|
||||||
}
|
}
|
||||||
@@ -67,8 +69,10 @@ func GenDeviceIDList(errorOnFail bool) *[]fs.DirEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WriteConfig writes the provided key-value pairs under the specified section headers in the libmutton.ini file.
|
// WriteConfig writes the provided key-value pairs under the specified section headers in the libmutton.ini file.
|
||||||
// Requires: valuesToWrite (a slice of length 3 arrays each containing a section, a key name, and a value).
|
// Requires: valuesToWrite (a slice of length 3 arrays each containing a section, a key name, and a value),
|
||||||
func WriteConfig(valuesToWrite [][3]string, append bool) {
|
// prune (a slice similar to valuesToWrite to allow removing the specified keys from an existing config)
|
||||||
|
// append (set to true to append to the existing libmutton.ini file, false to overwrite it).
|
||||||
|
func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool) {
|
||||||
var cfg *ini.File
|
var cfg *ini.File
|
||||||
|
|
||||||
if append {
|
if append {
|
||||||
@@ -94,10 +98,17 @@ func WriteConfig(valuesToWrite [][3]string, append bool) {
|
|||||||
section.Key(trio[1]).SetValue(trio[2])
|
section.Key(trio[1]).SetValue(trio[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prune specified keys from the existing config
|
||||||
|
if append && len(keysToPrune) > 0 {
|
||||||
|
// remove specified keys pairs from the existing config
|
||||||
|
for _, pair := range keysToPrune {
|
||||||
|
cfg.Section(pair[0]).DeleteKey(pair[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// save to libmutton.ini
|
// save to libmutton.ini
|
||||||
err := cfg.SaveTo(ConfigPath)
|
err := cfg.SaveTo(ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to save libmutton.ini:", err.Error()+AnsiReset)
|
PrintError("Failed to save libmutton.ini: "+err.Error(), ErrorWrite, true)
|
||||||
os.Exit(ErrorWrite)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-13
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CopyArgument copies a field from an entry to the clipboard.
|
// CopyArgument copies a field from an entry to the clipboard.
|
||||||
func CopyArgument(executableName, targetLocation string, field int) {
|
func CopyArgument(targetLocation string, field int) {
|
||||||
if isFile, _ := TargetIsFile(targetLocation, true, 2); isFile {
|
if isFile, _ := TargetIsFile(targetLocation, true, 2); isFile {
|
||||||
|
|
||||||
decryptedEntry := DecryptGPG(targetLocation)
|
decryptedEntry := DecryptGPG(targetLocation)
|
||||||
@@ -23,8 +23,7 @@ func CopyArgument(executableName, targetLocation string, field int) {
|
|||||||
|
|
||||||
// ensure field is not empty
|
// ensure field is not empty
|
||||||
if decryptedEntry[field] == "" {
|
if decryptedEntry[field] == "" {
|
||||||
fmt.Println(AnsiError + "Field is empty" + AnsiReset)
|
PrintError("Field is empty", ErrorTargetNotFound, true)
|
||||||
os.Exit(ErrorTargetNotFound)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if field != 2 {
|
if field != 2 {
|
||||||
@@ -42,35 +41,66 @@ func CopyArgument(executableName, targetLocation string, field int) {
|
|||||||
|
|
||||||
fmt.Println("Clipboard will be kept up to date with the current TOTP code until this process is closed")
|
fmt.Println("Clipboard will be kept up to date with the current TOTP code until this process is closed")
|
||||||
|
|
||||||
for { // keep field copied to clipboard, refresh on 30-second intervals
|
for { // keep token copied to clipboard, refresh on 30-second intervals
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
copyField("", GenTOTP(secret, currentTime, forSteam))
|
copyString(true, GenTOTP(secret, currentTime, forSteam))
|
||||||
// sleep until next 30-second interval
|
// sleep until next 30-second interval
|
||||||
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
|
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(AnsiError + "Field does not exist in entry" + AnsiReset)
|
PrintError("Field does not exist in entry", ErrorTargetNotFound, true)
|
||||||
os.Exit(ErrorTargetNotFound)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy field to clipboard, launch clipboard clearing process
|
// copy field to clipboard, launch clipboard clearing process
|
||||||
copyField(executableName, copySubject)
|
copyString(false, copySubject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClipClearArgument is called to clear the clipboard after 30 seconds if the contents have not been modified.
|
// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess.
|
||||||
func ClipClearArgument() {
|
func ClipClearArgument() {
|
||||||
// read previous clipboard contents from stdin
|
// read previous clipboard contents from stdin
|
||||||
clipScanner := bufio.NewScanner(os.Stdin)
|
clipScanner := bufio.NewScanner(os.Stdin)
|
||||||
if clipScanner.Scan() {
|
if clipScanner.Scan() {
|
||||||
oldContents := clipScanner.Text()
|
assignedContents := clipScanner.Text()
|
||||||
clipClear(oldContents)
|
clipClearProcess(assignedContents)
|
||||||
} else {
|
} else {
|
||||||
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clipClearProcess clears the clipboard after 30 seconds if the clipboard contents have not changed.
|
||||||
|
// assignedContents can be omitted to clear the clipboard immediately and unconditionally.
|
||||||
|
func clipClearProcess(assignedContents string) {
|
||||||
|
cmdPaste, cmdClear := getClipCommands()
|
||||||
|
|
||||||
|
clearClipboard := func() {
|
||||||
|
err := cmdClear.Run()
|
||||||
|
if err != nil {
|
||||||
|
PrintError("Failed to clear clipboard", ErrorClipboard, true)
|
||||||
|
}
|
||||||
|
Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if assignedContents is empty, clear the clipboard immediately and unconditionally
|
||||||
|
if assignedContents == "" {
|
||||||
|
clearClipboard()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait 30 seconds before checking clipboard contents
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
|
newContents, err := cmdPaste.Output()
|
||||||
|
if err != nil {
|
||||||
|
PrintError("Failed to read clipboard contents", ErrorClipboard, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||||
|
clearClipboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP).
|
// GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP).
|
||||||
func GenTOTP(secret string, time time.Time, forSteam bool) string {
|
func GenTOTP(secret string, time time.Time, forSteam bool) string {
|
||||||
var totpToken string
|
var totpToken string
|
||||||
@@ -83,8 +113,7 @@ func GenTOTP(secret string, time time.Time, forSteam bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError + "Error generating TOTP code" + AnsiReset)
|
PrintError("Error generating TOTP code", ErrorOther, true)
|
||||||
os.Exit(ErrorOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return totpToken
|
return totpToken
|
||||||
|
|||||||
+11
-40
@@ -3,55 +3,26 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyField copies a field from an entry to the clipboard.
|
// copyString copies a string to the clipboard.
|
||||||
func copyField(executableName, copySubject string) {
|
func copyString(continuous bool, copySubject string) {
|
||||||
cmd := exec.Command("pbcopy")
|
cmd := exec.Command("pbcopy")
|
||||||
writeToStdin(cmd, copySubject)
|
WriteToStdin(cmd, copySubject)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to copy to clipboard:", err.Error()+AnsiReset)
|
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// launch clipboard clearing process if executableName is provided
|
if !continuous {
|
||||||
if executableName != "" {
|
LaunchClipClearProcess(copySubject)
|
||||||
cmd = exec.Command(executableName, "clipclear")
|
|
||||||
writeToStdin(cmd, copySubject)
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds.
|
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||||
func clipClear(oldContents string) {
|
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||||
time.Sleep(30 * time.Second)
|
cmdClear := exec.Command("pbcopy")
|
||||||
|
WriteToStdin(cmdClear, "")
|
||||||
cmd := exec.Command("pbpaste")
|
return exec.Command("pbpaste"), cmdClear
|
||||||
newContents, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldContents == strings.TrimRight(string(newContents), "\r\n") {
|
|
||||||
cmd = exec.Command("pbcopy")
|
|
||||||
writeToStdin(cmd, "")
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-41
@@ -1,57 +1,28 @@
|
|||||||
//go:build linux && termux
|
//go:build android && termux
|
||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyField copies a field from an entry to the clipboard.
|
// copyString copies a string to the clipboard.
|
||||||
func copyField(executableName, copySubject string) {
|
func copyString(continuous bool, copySubject string) {
|
||||||
cmd := exec.Command("termux-clipboard-set")
|
cmd := exec.Command("termux-clipboard-set")
|
||||||
writeToStdin(cmd, copySubject)
|
WriteToStdin(cmd, copySubject)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to copy to clipboard:", err.Error()+AnsiReset)
|
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// launch clipboard clearing process if executableName is provided
|
if !continuous {
|
||||||
if executableName != "" {
|
LaunchClipClearProcess(copySubject)
|
||||||
cmd = exec.Command(executableName, "clipclear")
|
|
||||||
writeToStdin(cmd, copySubject)
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds.
|
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||||
func clipClear(oldContents string) {
|
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||||
time.Sleep(30 * time.Second)
|
cmdClear := exec.Command("termux-clipboard-set")
|
||||||
|
WriteToStdin(cmdClear, "")
|
||||||
cmd := exec.Command("termux-clipboard-get")
|
return exec.Command("termux-clipboard-get"), cmdClear
|
||||||
newContents, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldContents == strings.TrimRight(string(newContents), "\r\n") {
|
|
||||||
cmd = exec.Command("termux-clipboard-set")
|
|
||||||
writeToStdin(cmd, "")
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build !windows && !darwin && !android && !termux && !wsl
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// copyString copies a string to the clipboard.
|
||||||
|
func copyString(continuous bool, copySubject string) {
|
||||||
|
var envSet, isWayland bool // track whether environment variables are set
|
||||||
|
var cmdCopy *exec.Cmd
|
||||||
|
// determine whether to use wl-copy (Wayland) or xclip (X11)
|
||||||
|
if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet {
|
||||||
|
cmdCopy = exec.Command("wl-copy", "-t", "text/plain")
|
||||||
|
isWayland = true
|
||||||
|
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
|
||||||
|
cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain")
|
||||||
|
} else {
|
||||||
|
PrintError("Clipboard platform could not be determined", ErrorClipboard, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteToStdin(cmdCopy, copySubject)
|
||||||
|
err := cmdCopy.Run()
|
||||||
|
if err != nil {
|
||||||
|
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !continuous {
|
||||||
|
LaunchClipClearProcess(copySubject, isWayland)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||||
|
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||||
|
if os.Args[2] == "true" { // wayland
|
||||||
|
return exec.Command("wl-paste"), exec.Command("wl-copy", "-c")
|
||||||
|
}
|
||||||
|
return exec.Command("xclip", "-o", "-sel", "c"), exec.Command("xclip", "-i", "/dev/null", "-sel", "c")
|
||||||
|
}
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
//go:build !windows && !darwin && !termux && !wsl
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// copyField copies a field from an entry to the clipboard.
|
|
||||||
func copyField(executableName, copySubject string) {
|
|
||||||
var envSet bool // track whether environment variables are set
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
// determine whether to use wl-copy (Wayland) or xclip (X11)
|
|
||||||
if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet {
|
|
||||||
cmd = exec.Command("wl-copy", "-t", "text/plain")
|
|
||||||
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
|
|
||||||
cmd = exec.Command("xclip", "-sel", "c", "-t", "text/plain")
|
|
||||||
} else {
|
|
||||||
fmt.Println(AnsiError + "Clipboard platform could not be determined - Note that the clipboard does not function in a raw TTY" + AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeToStdin(cmd, copySubject)
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to copy to clipboard:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
// launch clipboard clearing process if executableName is provided
|
|
||||||
if executableName != "" {
|
|
||||||
cmd = exec.Command(executableName, "clipclear")
|
|
||||||
writeToStdin(cmd, copySubject)
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds.
|
|
||||||
func clipClear(oldContents string) {
|
|
||||||
time.Sleep(30 * time.Second)
|
|
||||||
|
|
||||||
// determine clipboard tool to use (wl-clipboard VS xclip)
|
|
||||||
var envSet bool
|
|
||||||
var cmdClear, cmdPaste *exec.Cmd
|
|
||||||
if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet {
|
|
||||||
cmdClear = exec.Command("wl-copy", "-c")
|
|
||||||
cmdPaste = exec.Command("wl-paste")
|
|
||||||
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
|
|
||||||
cmdClear = exec.Command("xclip", "-i", "/dev/null", "-sel", "c")
|
|
||||||
cmdPaste = exec.Command("xclip", "-o", "-sel", "c")
|
|
||||||
} else {
|
|
||||||
fmt.Println(AnsiError + "Clipboard platform could not be determined - Neither $WAYLAND_DISPLAY nor $DISPLAY are set" + AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read current clipboard contents
|
|
||||||
newContents, err := cmdPaste.Output()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear clipboard if contents have not been modified
|
|
||||||
if oldContents == strings.TrimRight(string(newContents), "\r\n") {
|
|
||||||
err = cmdClear.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
|
||||||
}
|
|
||||||
+8
-36
@@ -4,52 +4,24 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyField copies a field from an entry to the clipboard.
|
// copyString copies a string to the clipboard.
|
||||||
func copyField(executableName, copySubject string) {
|
func copyString(continuous bool, copySubject string) {
|
||||||
cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
|
cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to copy to clipboard:", err.Error()+AnsiReset)
|
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// launch clipboard clearing process if executableName is provided
|
if !continuous {
|
||||||
if executableName != "" {
|
LaunchClipClearProcess(copySubject)
|
||||||
cmd = exec.Command(executableName, "clipclear")
|
|
||||||
writeToStdin(cmd, copySubject)
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds.
|
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||||
func clipClear(oldContents string) {
|
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||||
time.Sleep(30 * time.Second)
|
return exec.Command("powershell.exe", "-c", "Get-Clipboard"), exec.Command("powershell.exe", "-c", "Set-Clipboard")
|
||||||
|
|
||||||
cmd := exec.Command("powershell.exe", "-c", "Get-Clipboard")
|
|
||||||
newContents, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldContents == strings.TrimRight(string(newContents), "\r\n") {
|
|
||||||
cmd = exec.Command("powershell.exe", "-c", "Set-Clipboard")
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset)
|
|
||||||
os.Exit(ErrorClipboard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
//go:build !returnOnExit
|
//go:build !interactive
|
||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
//go:build returnOnExit
|
//go:build interactive
|
||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
// Exit (soft) is meant to be used in interactive implementations (GUIs/TUIs) to keep the program running after an operation.
|
// Exit (soft) is meant to be used in interactive implementations (GUIs/TUIs) to keep the program running after an operation.
|
||||||
func Exit(code int) {
|
func Exit(code int) int {
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-10
@@ -1,8 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -18,22 +16,20 @@ func DecryptGPG(targetLocation string) []string {
|
|||||||
enableVirtualTerminalProcessing()
|
enableVirtualTerminalProcessing()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError + "Failed to decrypt \"" + targetLocation + "\" - Ensure it is a valid GPG-encrypted file and that you entered your passphrase correctly" + AnsiReset)
|
PrintError("Failed to decrypt \""+targetLocation+"\" - Ensure it is a valid GPG-encrypted file and that you entered your passphrase correctly", ErrorDecryption, true)
|
||||||
os.Exit(ErrorDecryption)
|
|
||||||
}
|
}
|
||||||
outputSlice := strings.Split(string(output), "\n")
|
|
||||||
|
|
||||||
return outputSlice
|
return strings.Split(string(output), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptGPG encrypts a slice of strings using GPG and returns the encrypted data as a byte slice.
|
// EncryptGPG encrypts a slice of strings using GPG and returns the encrypted data as a byte slice.
|
||||||
func EncryptGPG(input []string) []byte {
|
func EncryptGPG(input []string) []byte {
|
||||||
cmd := exec.Command("gpg", "-q", "-r", ParseConfig([][2]string{{"LIBMUTTON", "gpgID"}}, "")[0], "-e")
|
gpgCfg, _ := ParseConfig([][2]string{{"LIBMUTTON", "gpgID"}}, "")
|
||||||
writeToStdin(cmd, strings.Join(input, "\n"))
|
cmd := exec.Command("gpg", "-q", "-r", gpgCfg[0], "-e")
|
||||||
|
WriteToStdin(cmd, strings.Join(input, "\n"))
|
||||||
encryptedBytes, err := cmd.Output()
|
encryptedBytes, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError + "Failed to encrypt data - Ensure that your GPG key is valid and that you have a valid GPG ID set in libmutton.ini" + AnsiReset)
|
PrintError("Failed to encrypt data - Ensure that you have a valid GPG ID set in libmutton.ini", ErrorEncryption, true)
|
||||||
os.Exit(ErrorEncryption)
|
|
||||||
}
|
}
|
||||||
return encryptedBytes
|
return encryptedBytes
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-9
@@ -1,7 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -45,8 +44,7 @@ func GpgKeyGen() string {
|
|||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to generate GPG key:", err.Error()+AnsiReset)
|
PrintError("Failed to generate GPG key: "+err.Error(), ErrorOther, true)
|
||||||
os.Exit(ErrorOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "libmutton-" + unixTime + " (gpg-libmutton) <github.com/rwinkhart/libmutton>"
|
return "libmutton-" + unixTime + " (gpg-libmutton) <github.com/rwinkhart/libmutton>"
|
||||||
@@ -58,8 +56,7 @@ func DirInit(preserveOldConfigDir bool) string {
|
|||||||
// create EntryRoot
|
// create EntryRoot
|
||||||
err := os.MkdirAll(EntryRoot, 0700)
|
err := os.MkdirAll(EntryRoot, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to create \""+EntryRoot+"\":", err.Error()+AnsiReset)
|
PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), ErrorWrite, true)
|
||||||
os.Exit(ErrorWrite)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get old device ID before its potential removal
|
// get old device ID before its potential removal
|
||||||
@@ -77,8 +74,7 @@ func DirInit(preserveOldConfigDir bool) string {
|
|||||||
if isAccessible {
|
if isAccessible {
|
||||||
err = os.RemoveAll(ConfigDir)
|
err = os.RemoveAll(ConfigDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to remove existing config directory:", err.Error()+AnsiReset)
|
PrintError("Failed to remove existing config directory: "+err.Error(), ErrorWrite, true)
|
||||||
os.Exit(ErrorWrite)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,8 +82,7 @@ func DirInit(preserveOldConfigDir bool) string {
|
|||||||
// create config directory w/devices subdirectory
|
// create config directory w/devices subdirectory
|
||||||
err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700)
|
err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to create \""+ConfigDir+"\":", err.Error()+AnsiReset)
|
PrintError("Failed to create \""+ConfigDir+"\": "+err.Error(), ErrorWrite, true)
|
||||||
os.Exit(ErrorWrite)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return oldDeviceID
|
return oldDeviceID
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//go:build (windows || darwin || android || termux || wsl) && !interactive
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LaunchClipClearProcess launches the timed clipboard clearing process.
|
||||||
|
// For non-interactive CLI implementations, an entirely separate process is created for this purpose.
|
||||||
|
func LaunchClipClearProcess(copySubject string) {
|
||||||
|
executableName := os.Args[0]
|
||||||
|
cmd := exec.Command(executableName, "clipclear")
|
||||||
|
WriteToStdin(cmd, copySubject)
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
PrintError("Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?", ErrorClipboard, true)
|
||||||
|
}
|
||||||
|
os.Exit(0) // use os.Exit directly since this version of this function is only meant for non-interactive CLI implementations
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//go:build !windows && !darwin && !android && !termux && !wsl && !interactive
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LaunchClipClearProcess launches the timed clipboard clearing process.
|
||||||
|
// For non-interactive CLI implementations, an entirely separate process is created for this purpose.
|
||||||
|
func LaunchClipClearProcess(copySubject string, isWayland bool) {
|
||||||
|
executableName := os.Args[0]
|
||||||
|
cmd := exec.Command(executableName, "clipclear", strconv.FormatBool(isWayland))
|
||||||
|
WriteToStdin(cmd, copySubject)
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
PrintError("Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?", ErrorClipboard, true)
|
||||||
|
}
|
||||||
|
os.Exit(0) // use os.Exit directly since this version of this function is only meant for non-interactive CLI implementations
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build (windows || darwin || android || termux || wsl) && interactive
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
// LaunchClipClearProcess launches the timed clipboard clearing process.
|
||||||
|
// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine.
|
||||||
|
// copySubject can be omitted to clear the clipboard immediately and unconditionally.
|
||||||
|
func LaunchClipClearProcess(copySubject string) {
|
||||||
|
go clipClearProcess(copySubject)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build !windows && !darwin && !android && !termux && !wsl && interactive
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LaunchClipClearProcess launches the timed clipboard clearing process.
|
||||||
|
// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine.
|
||||||
|
// copySubject can be omitted to clear the clipboard immediately and unconditionally.
|
||||||
|
func LaunchClipClearProcess(copySubject string, isWayland bool) {
|
||||||
|
os.Args = []string{os.Args[0], "", strconv.FormatBool(isWayland)}
|
||||||
|
go clipClearProcess(copySubject)
|
||||||
|
}
|
||||||
+59
-24
@@ -18,50 +18,38 @@ func TargetIsFile(targetLocation string, errorOnFail bool, failCondition uint8)
|
|||||||
targetInfo, err := os.Stat(targetLocation)
|
targetInfo, err := os.Stat(targetLocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errorOnFail {
|
if errorOnFail {
|
||||||
fmt.Println(AnsiError + "Failed to access \"" + targetLocation + "\" - Ensure it exists and has the correct permissions" + AnsiReset)
|
PrintError("Failed to access \""+targetLocation+"\" - Ensure it exists and has the correct permissions", ErrorTargetNotFound, true)
|
||||||
os.Exit(ErrorTargetNotFound)
|
|
||||||
}
|
}
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
if targetInfo.IsDir() {
|
if targetInfo.IsDir() {
|
||||||
if errorOnFail && failCondition == 2 {
|
if errorOnFail && failCondition == 2 {
|
||||||
fmt.Println(AnsiError + "\"" + targetLocation + "\" is a directory" + AnsiReset)
|
PrintError("\""+targetLocation+"\" is a directory", ErrorTargetWrongType, true)
|
||||||
os.Exit(ErrorTargetWrongType)
|
|
||||||
}
|
}
|
||||||
return false, true
|
return false, true
|
||||||
} else {
|
} else {
|
||||||
if errorOnFail && failCondition == 1 {
|
if errorOnFail && failCondition == 1 {
|
||||||
fmt.Println(AnsiError + "\"" + targetLocation + "\" is a file" + AnsiReset)
|
PrintError("\""+targetLocation+"\" is a file", ErrorTargetWrongType, true)
|
||||||
os.Exit(ErrorTargetWrongType)
|
|
||||||
}
|
}
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteEntry writes entryData to an encrypted file at targetLocation.
|
// WriteEntry writes entryData to an encrypted file at targetLocation.
|
||||||
func WriteEntry(targetLocation string, entryData []string, verifyEntryDoesNotExist bool) {
|
func WriteEntry(targetLocation string, entryData []string) {
|
||||||
if verifyEntryDoesNotExist {
|
|
||||||
_, isAccessible := TargetIsFile(targetLocation, false, 0)
|
|
||||||
if isAccessible {
|
|
||||||
fmt.Println(AnsiError + "Target location already exists" + AnsiReset)
|
|
||||||
os.Exit(ErrorTargetExists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptedBytes := EncryptGPG(entryData)
|
encryptedBytes := EncryptGPG(entryData)
|
||||||
err := os.WriteFile(targetLocation, encryptedBytes, 0600)
|
err := os.WriteFile(targetLocation, encryptedBytes, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to write to file:", err.Error()+AnsiReset)
|
PrintError("Failed to write to file: "+err.Error(), ErrorWrite, true)
|
||||||
os.Exit(ErrorWrite)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeToStdin is a utility function that writes a string to a command's stdin.
|
// WriteToStdin is a utility function that writes a string to a command's stdin.
|
||||||
func writeToStdin(cmd *exec.Cmd, input string) {
|
// TODO unexport (import?) after migration off of GPG
|
||||||
|
func WriteToStdin(cmd *exec.Cmd, input string) {
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to access stdin for system command:", err.Error()+AnsiReset)
|
PrintError("Failed to access stdin for system command: "+err.Error(), ErrorOther, true)
|
||||||
os.Exit(ErrorOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -76,8 +64,7 @@ func writeToStdin(cmd *exec.Cmd, input string) {
|
|||||||
func CreateTempFile() *os.File {
|
func CreateTempFile() *os.File {
|
||||||
tempFile, err := os.CreateTemp("", "*.markdown")
|
tempFile, err := os.CreateTemp("", "*.markdown")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(AnsiError+"Failed to create temporary file:", err.Error()+AnsiReset)
|
PrintError("Failed to create temporary file: "+err.Error(), ErrorWrite, true)
|
||||||
os.Exit(ErrorWrite)
|
|
||||||
}
|
}
|
||||||
return tempFile
|
return tempFile
|
||||||
}
|
}
|
||||||
@@ -89,7 +76,37 @@ func RemoveTrailingEmptyStrings(slice []string) []string {
|
|||||||
return slice[:i+1]
|
return slice[:i+1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{}
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClampTrailingWhitespace strips trailing newlines, carriage returns, and tabs from each line in a note.
|
||||||
|
// Additionally, it removes single trailing spaces and truncates multiple trailing spaces to two (for Markdown formatting).
|
||||||
|
func ClampTrailingWhitespace(note []string) {
|
||||||
|
for i, line := range note {
|
||||||
|
// remove trailing tabs, carriage returns, and newlines
|
||||||
|
note[i] = strings.TrimRight(line, "\t\r\n")
|
||||||
|
|
||||||
|
// determine the number of trailing spaces
|
||||||
|
var endSpacesCount int
|
||||||
|
for j := len(line) - 1; j >= 0; j-- {
|
||||||
|
if line[j] != ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
endSpacesCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove single spaces, truncate multiple spaces (leave two for Markdown formatting)
|
||||||
|
switch endSpacesCount {
|
||||||
|
case 0:
|
||||||
|
// do nothing
|
||||||
|
case 1:
|
||||||
|
// remove the trailing space
|
||||||
|
note[i] = strings.TrimRight(line, " ")
|
||||||
|
default:
|
||||||
|
// truncate the trailing spaces to two
|
||||||
|
note[i] = line[:len(line)-endSpacesCount+2]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringGen generates a random string of a specified length and complexity.
|
// StringGen generates a random string of a specified length and complexity.
|
||||||
@@ -154,3 +171,21 @@ func EntryIsNotEmpty(entryData []string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpandPathWithHome, given a path (as a string) containing "~", returns the path with "~" expanded to the user's home directory.
|
||||||
|
func ExpandPathWithHome(path string) string {
|
||||||
|
return strings.Replace(path, "~", Home, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintError prints an error message in the standard libmutton format and exits with the specified exit code.
|
||||||
|
// Requires: message (the error message to print),
|
||||||
|
// exitCode (the exit code to use),
|
||||||
|
// forceHardExit (if true, exit immediately; if false, allow soft exit for interactive clients).
|
||||||
|
func PrintError(message string, exitCode int, forceHardExit bool) {
|
||||||
|
fmt.Println(AnsiError + message + AnsiReset)
|
||||||
|
if forceHardExit {
|
||||||
|
os.Exit(exitCode)
|
||||||
|
} else {
|
||||||
|
Exit(exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
**libmutton v0.3.0**
|
||||||
|
February 22, 2025
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- (f9aa2fc374b77fc8325d0c7644ffc9c96169c3c6) (d2034b458b702b8f1664bc188c1840040ec0f704) (81b78068a75a23c73be607cbe6d4dc8b1539f831) `core.LaunchClipClearProcess`, `core.WriteToStdin`, `core.ExpandPathWithHome`, and `core.PrintError` are now exported utility functions for direct use by clients
|
||||||
|
- (c28d45c9da94ec45d89dbda6d645042a7fdbcd29) One-off sync functions can now be forced to run in offline mode
|
||||||
|
- (5028fb21b019e8a867d031463f5de1402fc053f4) SSH connection attempts now have a 3-second timeout
|
||||||
|
- (89b74cec1e014dba31e2b728e3544941d6edd2c2) Individual keys can now be removed from the config file
|
||||||
|
- Many features for building interactive (GUI/TUI) clients
|
||||||
|
- (619e4220a60fc0cb284f5640eaf9af86c0651ac7) Clipboard clearing can now be handled much more sensibly by interactive clients
|
||||||
|
- (3b57d59ddcf0694b2bbae8c86ec06faff299e950) (55be8a30759ee5d12628c631af94a57d582281a4) The clipboard can now be cleared instantly (no timer); useful for clearing on client exit
|
||||||
|
- (2feeeb74d7813dbbb75795d5ccdd817bbcea3602) `core.ParseConfig` now returns errors for proper handling in interactive clients
|
||||||
|
- (68e3108741fae0aaf316dcf33a5074b0e19a7b4f) `sync.RunJob` can now return lists of synchronized entries for display in interactive clients
|
||||||
|
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
- (eb2b349697ede136ea031cf7d82bfb72a5a0dcf9) Deletions are now synchronized before folders to avoid sync failures under unlikely conditions
|
||||||
|
- (fc1cd349b8c5468afa3feaf7b5b9042eb4c467e7) Double-space line breaks with Markdown formatting are now preserved when saving an entry
|
||||||
|
- (4689cae3f4a1e19486935f2da5b06bf0a90c731c) Sheared folder names now always end in a trailing slash for clarity and reduced duplicate entries in the deletions directory
|
||||||
|
- Many fixes for building interactive (GUI/TUI) clients
|
||||||
|
- (1cebb395460fb6068ebf4253ec8bd93e9d2b2a96) The soft exit function now specifies a return value
|
||||||
|
- (a4a39a3a995d1560514dc3b782abb26329214c3f) Passphrase-protected SSH identity files are no longer broken for interactive clients
|
||||||
|
- (1c650d475145c3db4e370576d89eb77abe6e5e52) (0162b737c84d42e168803e0c074103b5c0676384) Interactive clients will no longer crash on SSH dialing failures
|
||||||
|
- (173574eb85b3172414d104a2bf391a3bce4ddc95) Error messages not making sense for interactive clients have been addressed
|
||||||
|
|
||||||
|
## Optimizations
|
||||||
|
- (f2f9c523f441b49d682795c01454167d68aae573) An unnecessary variable declaration was removed in `core.DecryptGPG`
|
||||||
|
- (fb5449cf1f75aa0f93c0484d590abca04f977d7a) A redundant (and late) check for the pre-existence of a new entry has been removed
|
||||||
|
- (e9d0c3704362c8963e63a6c3b70d036f3e6c2493) `core.PrintError` has been used to decrease the overall binary size through improved code re-use
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- Bumps (direct and indirect)
|
||||||
|
- Go: v1.23.4 => v1.24.0
|
||||||
|
- golang.org/x/crypto: v0.31.0 => v0.34.0
|
||||||
|
- golang.org/x/sys: v0.28.0 => v0.30.0
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
module github.com/rwinkhart/libmutton
|
module github.com/rwinkhart/libmutton
|
||||||
|
|
||||||
go 1.23.4
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727
|
github.com/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727
|
||||||
github.com/pkg/sftp v1.13.7
|
github.com/pkg/sftp v1.13.7
|
||||||
github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481
|
github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.34.0
|
||||||
golang.org/x/term v0.27.0
|
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boombuler/barcode v1.0.2 // indirect
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -44,15 +44,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
|||||||
+2
-4
@@ -77,7 +77,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func helpServer() {
|
func helpServer() {
|
||||||
fmt.Print(ansiBold + "\nlibmuttonserver | Copyright (c) 2024 Randall Winkhart\n" + core.AnsiReset + `
|
fmt.Print(ansiBold + "\nlibmuttonserver | Copyright (c) 2024-2025 Randall Winkhart\n" + core.AnsiReset + `
|
||||||
This software exists under the MIT license; you may redistribute it under certain conditions.
|
This software exists under the MIT license; you may redistribute it under certain conditions.
|
||||||
This program comes with absolutely no warranty; type "libmuttonserver version" for details.
|
This program comes with absolutely no warranty; type "libmuttonserver version" for details.
|
||||||
|
|
||||||
@@ -116,7 +116,5 @@ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|||||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
OR OTHER DEALINGS IN THE SOFTWARE.` + "\n\n---------------------------------------------------------")
|
OR OTHER DEALINGS IN THE SOFTWARE.` + "\n\n---------------------------------------------------------")
|
||||||
fmt.Print(ansiBold + "\n\n libmuttonserver" + core.AnsiReset + " Version " + core.LibmuttonVersion + `
|
fmt.Print(ansiBold + "\n\n libmuttonserver" + core.AnsiReset + " Version " + core.LibmuttonVersion + "\n\n Copyright (c) 2024-2025: Randall Winkhart" + "\n\n")
|
||||||
|
|
||||||
Copyright (c) 2024 Randall Winkhart` + "\n\n")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-65
@@ -27,11 +27,11 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
var sshUserConfig []string
|
var sshUserConfig []string
|
||||||
var missingValueError string
|
var missingValueError string
|
||||||
if manualSync {
|
if manualSync {
|
||||||
missingValueError = joinErrorWithEXE("SSH settings not configured - Run \"", " init\" to configure")
|
missingValueError = "SSH settings not fully configured"
|
||||||
} else {
|
} else {
|
||||||
missingValueError = "0"
|
missingValueError = "0" // allow silent exit at this point in offline mode
|
||||||
}
|
}
|
||||||
sshUserConfig = core.ParseConfig([][2]string{{"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshIsWindows"}}, missingValueError)
|
sshUserConfig, _ = core.ParseConfig([][2]string{{"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshIsWindows"}}, missingValueError)
|
||||||
|
|
||||||
var user, ip, port, keyFile, keyFileProtected, entryRoot string
|
var user, ip, port, keyFile, keyFileProtected, entryRoot string
|
||||||
var isWindows bool
|
var isWindows bool
|
||||||
@@ -53,8 +53,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
case 6:
|
case 6:
|
||||||
isWindows, err = strconv.ParseBool(key)
|
isWindows, err = strconv.ParseBool(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to parse server OS type:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to parse server OS type: "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,8 +61,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
// read private key
|
// read private key
|
||||||
key, err := os.ReadFile(keyFile)
|
key, err := os.ReadFile(keyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to read private key file:", keyFile+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to read private key: "+keyFile, core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse private key
|
// parse private key
|
||||||
@@ -71,19 +69,17 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
if keyFileProtected != "true" {
|
if keyFileProtected != "true" {
|
||||||
parsedKey, err = ssh.ParsePrivateKey(key)
|
parsedKey, err = ssh.ParsePrivateKey(key)
|
||||||
} else {
|
} else {
|
||||||
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, inputKeyFilePassphrase())
|
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, core.PassphraseInputFunction("Enter passphrase for your SSH keyfile:"))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to parse private key:", keyFile+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to parse private key: "+keyFile, core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// read known hosts file
|
// read known hosts file
|
||||||
var hostKeyCallback ssh.HostKeyCallback
|
var hostKeyCallback ssh.HostKeyCallback
|
||||||
hostKeyCallback, err = knownhosts.New(core.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts")
|
hostKeyCallback, err = knownhosts.New(core.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to read known hosts file:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to read known hosts file: "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// configure SSH client
|
// configure SSH client
|
||||||
@@ -93,13 +89,14 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
ssh.PublicKeys(parsedKey),
|
ssh.PublicKeys(parsedKey),
|
||||||
},
|
},
|
||||||
HostKeyCallback: hostKeyCallback,
|
HostKeyCallback: hostKeyCallback,
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to SSH server
|
// connect to SSH server
|
||||||
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to connect to remote server:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to connect to remote server: "+err.Error(), core.ErrorServerConnection, false) // do not crash/close interactive clients
|
||||||
os.Exit(core.ErrorServerConnection)
|
return nil, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshClient, entryRoot, isWindows
|
return sshClient, entryRoot, isWindows
|
||||||
@@ -110,8 +107,7 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string {
|
|||||||
// create a session
|
// create a session
|
||||||
sshSession, err := sshClient.NewSession()
|
sshSession, err := sshClient.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to establish SSH session:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to establish SSH session: "+err.Error(), core.ErrorServerConnection, true)
|
||||||
os.Exit(core.ErrorServerConnection)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// provide stdin data for session
|
// provide stdin data for session
|
||||||
@@ -121,8 +117,7 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string {
|
|||||||
var output []byte
|
var output []byte
|
||||||
output, err = sshSession.CombinedOutput(cmd)
|
output, err = sshSession.CombinedOutput(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to run SSH command:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to run SSH command: "+err.Error(), core.ErrorSyncProcess, true)
|
||||||
os.Exit(core.ErrorSyncProcess)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert the output to a string and remove leading/trailing whitespace
|
// convert the output to a string and remove leading/trailing whitespace
|
||||||
@@ -138,8 +133,7 @@ func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string
|
|||||||
deviceIDList := core.GenDeviceIDList(true)
|
deviceIDList := core.GenDeviceIDList(true)
|
||||||
if len(*deviceIDList) == 0 {
|
if len(*deviceIDList) == 0 {
|
||||||
if manualSync {
|
if manualSync {
|
||||||
fmt.Println(joinErrorWithEXE("Sync failed - No device ID found; run \"", " init\" to generate a device ID"))
|
core.PrintError("Sync failed - No device ID found", core.ErrorTargetNotFound, true)
|
||||||
os.Exit(core.ErrorTargetNotFound)
|
|
||||||
} else {
|
} else {
|
||||||
core.Exit(0) // exit silently if the sync job was called automatically, as the user may just be in offline mode
|
core.Exit(0) // exit silently if the sync job was called automatically, as the user may just be in offline mode
|
||||||
}
|
}
|
||||||
@@ -152,13 +146,11 @@ func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string
|
|||||||
|
|
||||||
// parse output/re-form lists
|
// parse output/re-form lists
|
||||||
if len(outputSlice) != 5 { // ensure information from server is complete
|
if len(outputSlice) != 5 { // ensure information from server is complete
|
||||||
fmt.Println(core.AnsiError + "Sync failed - Unable to fetch remote data; server returned an unexpected response" + core.AnsiReset)
|
core.PrintError("Sync failed - Unable to fetch remote data; server returned an unexpected response", core.ErrorSyncProcess, true)
|
||||||
os.Exit(core.ErrorSyncProcess)
|
|
||||||
}
|
}
|
||||||
serverTime, err := strconv.ParseInt(outputSlice[0], 10, 64)
|
serverTime, err := strconv.ParseInt(outputSlice[0], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to parse server time:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to parse server time: "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
entries := strings.Split(outputSlice[1], core.FSMisc)[1:]
|
entries := strings.Split(outputSlice[1], core.FSMisc)[1:]
|
||||||
modsStrings := strings.Split(outputSlice[2], core.FSMisc)[1:]
|
modsStrings := strings.Split(outputSlice[2], core.FSMisc)[1:]
|
||||||
@@ -171,8 +163,7 @@ func getRemoteDataFromClient(sshClient *ssh.Client, manualSync bool) (map[string
|
|||||||
for _, modString := range modsStrings {
|
for _, modString := range modsStrings {
|
||||||
mod, err = strconv.ParseInt(modString, 10, 64)
|
mod, err = strconv.ParseInt(modString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to parse mod time:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to parse mod time: "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
mods = append(mods, mod)
|
mods = append(mods, mod)
|
||||||
}
|
}
|
||||||
@@ -218,14 +209,12 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
// create an SFTP client from sshClient
|
// create an SFTP client from sshClient
|
||||||
sftpClient, err := sftp.NewClient(sshClient)
|
sftpClient, err := sftp.NewClient(sshClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to establish SFTP session:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to establish SFTP session: "+err.Error(), core.ErrorServerConnection, true)
|
||||||
os.Exit(core.ErrorServerConnection)
|
|
||||||
}
|
}
|
||||||
defer func(sftpClient *sftp.Client) {
|
defer func(sftpClient *sftp.Client) {
|
||||||
err = sftpClient.Close()
|
err = sftpClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SFTP client:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to close SFTP client: "+err.Error(), core.ErrorServerConnection, true)
|
||||||
os.Exit(core.ErrorServerConnection)
|
|
||||||
}
|
}
|
||||||
}(sftpClient)
|
}(sftpClient)
|
||||||
|
|
||||||
@@ -243,8 +232,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
var fileInfo os.FileInfo
|
var fileInfo os.FileInfo
|
||||||
fileInfo, err = sftpClient.Stat(remoteEntryFullPath)
|
fileInfo, err = sftpClient.Stat(remoteEntryFullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to get remote file info (mod time):", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to get remote file info (mod time): "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
modTime := fileInfo.ModTime()
|
modTime := fileInfo.ModTime()
|
||||||
|
|
||||||
@@ -252,8 +240,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
var remoteFile *sftp.File
|
var remoteFile *sftp.File
|
||||||
remoteFile, err = sftpClient.Open(remoteEntryFullPath)
|
remoteFile, err = sftpClient.Open(remoteEntryFullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to open remote file:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to open remote file: "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// store path to local entry
|
// store path to local entry
|
||||||
@@ -263,15 +250,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
var localFile *os.File
|
var localFile *os.File
|
||||||
localFile, err = os.OpenFile(localEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
localFile, err = os.OpenFile(localEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to create local file:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to create local file: "+err.Error(), core.ErrorWrite, true)
|
||||||
os.Exit(core.ErrorWrite)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// download the file
|
// download the file
|
||||||
_, err = remoteFile.WriteTo(localFile)
|
_, err = remoteFile.WriteTo(localFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to download remote file:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to download remote file: "+err.Error(), core.ErrorSyncProcess, true)
|
||||||
os.Exit(core.ErrorSyncProcess)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// close the files
|
// close the files
|
||||||
@@ -300,8 +285,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
var fileInfo os.FileInfo
|
var fileInfo os.FileInfo
|
||||||
fileInfo, err = os.Stat(localEntryFullPath)
|
fileInfo, err = os.Stat(localEntryFullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to get local file info (mod time):", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to get local file info (mod time): "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
modTime := fileInfo.ModTime()
|
modTime := fileInfo.ModTime()
|
||||||
|
|
||||||
@@ -309,8 +293,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
var localFile *os.File
|
var localFile *os.File
|
||||||
localFile, err = os.Open(localEntryFullPath)
|
localFile, err = os.Open(localEntryFullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to open local file:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to open local file: "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// store path to remote entry
|
// store path to remote entry
|
||||||
@@ -320,15 +303,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
var remoteFile *sftp.File
|
var remoteFile *sftp.File
|
||||||
remoteFile, err = sftpClient.OpenFile(remoteEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY)
|
remoteFile, err = sftpClient.OpenFile(remoteEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to create remote file ("+remoteEntryFullPath+"):", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to create remote file ("+remoteEntryFullPath+"): "+err.Error(), core.ErrorWrite, true)
|
||||||
os.Exit(core.ErrorWrite)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload the file
|
// upload the file
|
||||||
_, err = localFile.WriteTo(remoteFile)
|
_, err = localFile.WriteTo(remoteFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to upload local file:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to upload local file: "+err.Error(), core.ErrorSyncProcess, true)
|
||||||
os.Exit(core.ErrorSyncProcess)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// close the files
|
// close the files
|
||||||
@@ -338,8 +319,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
// set permissions on remote file
|
// set permissions on remote file
|
||||||
err = sftpClient.Chmod(remoteEntryFullPath, 0600)
|
err = sftpClient.Chmod(remoteEntryFullPath, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to set permissions on remote file:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to set permissions on remote file: "+err.Error(), core.ErrorSyncProcess, true)
|
||||||
os.Exit(core.ErrorSyncProcess)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -353,7 +333,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
|||||||
|
|
||||||
// syncLists determines which entries need to be downloaded and uploaded for synchronizations and calls sftpSync with this information.
|
// syncLists determines which entries need to be downloaded and uploaded for synchronizations and calls sftpSync with this information.
|
||||||
// Using maps means that syncing will be done in an arbitrary order, but it is a worthy tradeoff for speed and simplicity.
|
// Using maps means that syncing will be done in an arbitrary order, but it is a worthy tradeoff for speed and simplicity.
|
||||||
func syncLists(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows, timeSynced bool, localEntryModMap, remoteEntryModMap map[string]int64) {
|
func syncLists(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows, timeSynced, returnLists bool, localEntryModMap, remoteEntryModMap map[string]int64) [3][]string {
|
||||||
// initialize slices to store entries that need to be downloaded or uploaded
|
// initialize slices to store entries that need to be downloaded or uploaded
|
||||||
var downloadList, uploadList []string
|
var downloadList, uploadList []string
|
||||||
|
|
||||||
@@ -393,6 +373,11 @@ func syncLists(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows, timeSyn
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Client is synchronized with server")
|
fmt.Println("Client is synchronized with server")
|
||||||
|
|
||||||
|
if returnLists {
|
||||||
|
return [3][]string{nil, downloadList, uploadList}
|
||||||
|
}
|
||||||
|
return [3][]string{nil, nil, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
// deletionSync removes entries from the client that have been deleted on the server (multi-client deletion).
|
// deletionSync removes entries from the client that have been deleted on the server (multi-client deletion).
|
||||||
@@ -403,8 +388,7 @@ func deletionSync(deletions []string) {
|
|||||||
fmt.Println(ansiDelete+deletion+core.AnsiReset, "has been sheared, removing locally (if it exists)")
|
fmt.Println(ansiDelete+deletion+core.AnsiReset, "has been sheared, removing locally (if it exists)")
|
||||||
err := os.RemoveAll(core.TargetLocationFormat(deletion))
|
err := os.RemoveAll(core.TargetLocationFormat(deletion))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Failed to shear "+deletion+" locally:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Failed to shear "+deletion+" locally: "+err.Error(), core.ErrorWrite, true)
|
||||||
os.Exit(core.ErrorWrite)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,38 +409,39 @@ func folderSync(folders []string) {
|
|||||||
if !isFile && !isAccessible {
|
if !isFile && !isAccessible {
|
||||||
err := os.MkdirAll(folderFullPath, 0700)
|
err := os.MkdirAll(folderFullPath, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Failed to create folder \""+folder+"\":", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Failed to create folder ("+folder+"): "+err.Error(), core.ErrorWrite, true)
|
||||||
os.Exit(core.ErrorWrite)
|
|
||||||
}
|
}
|
||||||
} else if isFile {
|
} else if isFile {
|
||||||
fmt.Println(core.AnsiError + "Sync failed - Failed to create folder \"" + folder + "\" - A file with the same name already exists" + core.AnsiReset)
|
core.PrintError("Sync failed - Failed to create folder ("+folder+") - A file with the same name already exists", core.ErrorTargetExists, true)
|
||||||
os.Exit(core.ErrorTargetExists)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunJob runs the SSH sync job.
|
// RunJob runs the SSH sync job.
|
||||||
// Setting manualSync to true will throw errors if sync is not configured (online mode is assumed).
|
// Setting manualSync to true will throw errors if sync is not configured (online mode is assumed).
|
||||||
func RunJob(manualSync bool) {
|
// Setting returnLists to true will return the deletions, downloads, and uploads lists for use by the client.
|
||||||
|
func RunJob(manualSync, returnLists bool) [3][]string {
|
||||||
// get SSH client to re-use throughout the sync process
|
// get SSH client to re-use throughout the sync process
|
||||||
sshClient, sshEntryRoot, sshIsWindows := GetSSHClient(manualSync)
|
sshClient, sshEntryRoot, sshIsWindows := GetSSHClient(manualSync)
|
||||||
|
if sshClient == nil { // indicate SSH dialing failure for interactive clients
|
||||||
|
return [3][]string{nil, nil, nil}
|
||||||
|
}
|
||||||
defer func(sshClient *ssh.Client) {
|
defer func(sshClient *ssh.Client) {
|
||||||
err := sshClient.Close()
|
err := sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||||
os.Exit(core.ErrorServerConnection)
|
|
||||||
}
|
}
|
||||||
}(sshClient)
|
}(sshClient)
|
||||||
|
|
||||||
// fetch remote lists
|
// fetch remote lists
|
||||||
remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime := getRemoteDataFromClient(sshClient, manualSync)
|
remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime := getRemoteDataFromClient(sshClient, manualSync)
|
||||||
|
|
||||||
// sync folders
|
|
||||||
folderSync(remoteFolders)
|
|
||||||
|
|
||||||
// sync deletions
|
// sync deletions
|
||||||
deletionSync(deletions)
|
deletionSync(deletions)
|
||||||
|
|
||||||
|
// sync folders
|
||||||
|
folderSync(remoteFolders)
|
||||||
|
|
||||||
// fetch local lists
|
// fetch local lists
|
||||||
localEntryModMap := getLocalData()
|
localEntryModMap := getLocalData()
|
||||||
|
|
||||||
@@ -469,8 +454,13 @@ func RunJob(manualSync bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sync new and updated entries
|
// sync new and updated entries
|
||||||
syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, localEntryModMap, remoteEntryModMap)
|
var lists [3][]string
|
||||||
|
if returnLists {
|
||||||
// exit program after successful sync
|
lists = syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap)
|
||||||
core.Exit(0)
|
lists[0] = deletions
|
||||||
|
return lists
|
||||||
|
}
|
||||||
|
syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, false, localEntryModMap, remoteEntryModMap)
|
||||||
|
core.Exit(0) // exit program if running non-interactively
|
||||||
|
return lists // dummy return for when not returning lists
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-16
@@ -1,7 +1,6 @@
|
|||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -20,10 +19,11 @@ func getModTimes(entryList []string) []int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ShearLocal removes the target file or directory from the local system.
|
// ShearLocal removes the target file or directory from the local system.
|
||||||
// Returns: deviceID (only on client; for use in ShearRemoteFromClient).
|
// Returns: deviceID (only on client; for use in ShearRemoteFromClient),
|
||||||
|
// isDir (only on client; for use in ShearRemoteFromClient).
|
||||||
// If the local system is a server, it will also add the target to the deletions list for all clients (except the requesting client).
|
// If the local system is a server, it will also add the target to the deletions list for all clients (except the requesting client).
|
||||||
// This function should only be used directly by the server binary.
|
// This function should only be used directly by the server binary.
|
||||||
func ShearLocal(targetLocationIncomplete, clientDeviceID string) string {
|
func ShearLocal(targetLocationIncomplete, clientDeviceID string) (string, bool) {
|
||||||
// determine if running on a server
|
// determine if running on a server
|
||||||
var onServer bool
|
var onServer bool
|
||||||
if clientDeviceID != "" {
|
if clientDeviceID != "" {
|
||||||
@@ -49,19 +49,19 @@ 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 := core.TargetLocationFormat(targetLocationIncomplete)
|
targetLocationComplete := core.TargetLocationFormat(targetLocationIncomplete)
|
||||||
|
var isFile bool
|
||||||
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
|
||||||
core.TargetIsFile(targetLocationComplete, true, 0)
|
isFile, _ = core.TargetIsFile(targetLocationComplete, true, 0)
|
||||||
}
|
}
|
||||||
err := os.RemoveAll(targetLocationComplete)
|
err := os.RemoveAll(targetLocationComplete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Failed to remove local target:", err.Error()+core.AnsiReset)
|
core.PrintError("Failed to remove local target: "+err.Error(), core.ErrorWrite, true)
|
||||||
os.Exit(core.ErrorWrite)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !onServer && len(*deviceIDList) > 0 { // return the device ID if running on the client and a device ID exists (online mode)
|
if !onServer && len(*deviceIDList) > 0 { // return the device ID if running on the client and a device ID exists (online mode)
|
||||||
return (*deviceIDList)[0].Name()
|
return (*deviceIDList)[0].Name(), !isFile
|
||||||
}
|
}
|
||||||
return ""
|
return "", true
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -80,15 +80,13 @@ func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldL
|
|||||||
// ensure newLocation does not exist
|
// ensure newLocation does not exist
|
||||||
_, isAccessible := core.TargetIsFile(newLocation, false, 0)
|
_, isAccessible := core.TargetIsFile(newLocation, false, 0)
|
||||||
if isAccessible {
|
if isAccessible {
|
||||||
fmt.Println(core.AnsiError + "\"" + newLocation + "\" already exists" + core.AnsiReset)
|
core.PrintError("\""+newLocation+"\" already exists", core.ErrorTargetExists, true)
|
||||||
os.Exit(core.ErrorTargetExists)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// rename oldLocation to newLocation
|
// rename oldLocation to newLocation
|
||||||
err := os.Rename(oldLocation, newLocation)
|
err := os.Rename(oldLocation, newLocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError + "Failed to rename - Does the target containing directory exist?" + core.AnsiReset)
|
core.PrintError("Failed to rename - Does the target containing directory exist?", core.ErrorTargetNotFound, true)
|
||||||
os.Exit(core.ErrorTargetNotFound)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not exit program, as this function is used as part of RenameRemoteFromClient
|
// do not exit program, as this function is used as part of RenameRemoteFromClient
|
||||||
@@ -102,11 +100,9 @@ func AddFolderLocal(targetLocationIncomplete string) {
|
|||||||
err := os.Mkdir(targetLocationComplete, 0700)
|
err := os.Mkdir(targetLocationComplete, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsExist(err) {
|
if os.IsExist(err) {
|
||||||
fmt.Println(core.AnsiError + "Directory already exists" + core.AnsiReset)
|
core.PrintError("Directory already exists", core.ErrorTargetExists, true)
|
||||||
os.Exit(core.ErrorTargetExists)
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(core.AnsiError+"Failed to create directory:", err.Error()+core.AnsiReset)
|
core.PrintError("Failed to create directory: "+err.Error(), core.ErrorWrite, true)
|
||||||
os.Exit(core.ErrorWrite)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -3,7 +3,6 @@
|
|||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -25,12 +24,10 @@ func WalkEntryDir() ([]string, []string) {
|
|||||||
// check for errors encountered while walking directory
|
// check for errors encountered while walking directory
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
fmt.Println(core.AnsiError+"The entry directory does not exist - Run \""+os.Args[0], "init"+"\" to create it"+core.AnsiReset)
|
core.PrintError("The entry directory does not exist - Initialize libmutton to create it", core.ErrorOther, true)
|
||||||
} else {
|
} else {
|
||||||
// otherwise, print the source of the error
|
core.PrintError("An unexpected error occurred while generating the entry list: "+err.Error(), core.ErrorOther, true)
|
||||||
fmt.Println(core.AnsiError+"An unexpected error occurred while generating the entry list:", err.Error()+core.AnsiReset)
|
|
||||||
}
|
}
|
||||||
os.Exit(core.ErrorOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// trim root path from each path before storing
|
// trim root path from each path before storing
|
||||||
@@ -48,8 +45,3 @@ func WalkEntryDir() ([]string, []string) {
|
|||||||
|
|
||||||
return fileList, dirList
|
return fileList, dirList
|
||||||
}
|
}
|
||||||
|
|
||||||
// joinErrorWithEXE is a utility function that joins and returns the two strings it is provided (in error format) with the executable name inserted between them.
|
|
||||||
func joinErrorWithEXE(firstHalf, secondHalf string) string {
|
|
||||||
return core.AnsiError + firstHalf + os.Args[0] + secondHalf + core.AnsiReset
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-10
@@ -3,7 +3,6 @@
|
|||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -26,12 +25,10 @@ func WalkEntryDir() ([]string, []string) {
|
|||||||
// check for errors encountered while walking directory
|
// check for errors encountered while walking directory
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
fmt.Println(joinErrorWithEXE("The entry directory does not exist - Run \"", " init"+"\" to create it"))
|
core.PrintError("The entry directory does not exist - Initialize libmutton to create it", core.ErrorOther, true)
|
||||||
} else {
|
} else {
|
||||||
// otherwise, print the source of the error
|
core.PrintError("An unexpected error occurred while generating the entry list: "+err.Error(), core.ErrorOther, true)
|
||||||
fmt.Println(core.AnsiError+"An unexpected error occurred while generating the entry list:", err.Error()+core.AnsiReset)
|
|
||||||
}
|
}
|
||||||
os.Exit(core.ErrorOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// trim root path from each path before storing and replace backslashes with forward slashes
|
// trim root path from each path before storing and replace backslashes with forward slashes
|
||||||
@@ -49,8 +46,3 @@ func WalkEntryDir() ([]string, []string) {
|
|||||||
|
|
||||||
return fileList, dirList
|
return fileList, dirList
|
||||||
}
|
}
|
||||||
|
|
||||||
// joinErrorWithEXE is a utility function that joins and returns the two strings it is provided (in error format) with the executable name inserted between them.
|
|
||||||
func joinErrorWithEXE(firstHalf, secondHalf string) string {
|
|
||||||
return core.AnsiError + firstHalf + os.Args[0][strings.LastIndex(os.Args[0], "\\")+1:] + secondHalf + core.AnsiReset
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-7
@@ -1,7 +1,6 @@
|
|||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -24,8 +23,7 @@ func DeviceIDGen(oldDeviceID string) (string, string) {
|
|||||||
// create new device ID file (locally)
|
// create new device ID file (locally)
|
||||||
fileToClose, err := os.OpenFile(core.ConfigDir+core.PathSeparator+"devices"+core.PathSeparator+newDeviceID, os.O_CREATE|os.O_WRONLY, 0600)
|
fileToClose, err := os.OpenFile(core.ConfigDir+core.PathSeparator+"devices"+core.PathSeparator+newDeviceID, os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Failed to create local device ID file:", err.Error()+core.AnsiReset)
|
core.PrintError("Failed to create local device ID file: "+err.Error(), core.ErrorWrite, true)
|
||||||
os.Exit(core.ErrorWrite)
|
|
||||||
}
|
}
|
||||||
_ = fileToClose.Close() // error ignored; if the file could be created, it can probably be closed
|
_ = fileToClose.Close() // error ignored; if the file could be created, it can probably be closed
|
||||||
|
|
||||||
@@ -36,15 +34,13 @@ func DeviceIDGen(oldDeviceID string) (string, string) {
|
|||||||
sshEntryRootSSHIsWindows := strings.Split(GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), core.FSSpace)
|
sshEntryRootSSHIsWindows := strings.Split(GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), core.FSSpace)
|
||||||
err = sshClient.Close()
|
err = sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Init failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
core.PrintError("Init failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||||
os.Exit(core.ErrorServerConnection)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove old device ID file (locally; may not exist)
|
// remove old device ID file (locally; may not exist)
|
||||||
err = os.RemoveAll(core.ConfigDir + core.PathSeparator + "devices" + core.PathSeparator + oldDeviceID)
|
err = os.RemoveAll(core.ConfigDir + core.PathSeparator + "devices" + core.PathSeparator + oldDeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Failed to remove old device ID file (locally):", err.Error()+core.AnsiReset)
|
core.PrintError("Failed to remove old device ID file (locally): "+err.Error(), core.ErrorWrite, true)
|
||||||
os.Exit(core.ErrorWrite)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1]
|
return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1]
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package sync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
// inputKeyFilePassphrase prompts the user for a passphrase for an SSH key file.
|
|
||||||
// TODO support non-CLI implementations
|
|
||||||
func inputKeyFilePassphrase() []byte {
|
|
||||||
fmt.Print("\nEnter passphrase for your SSH keyfile: ")
|
|
||||||
passphrase, _ := term.ReadPassword(int(os.Stdin.Fd()))
|
|
||||||
fmt.Println()
|
|
||||||
return passphrase
|
|
||||||
}
|
|
||||||
+15
-15
@@ -1,8 +1,6 @@
|
|||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rwinkhart/libmutton/core"
|
"github.com/rwinkhart/libmutton/core"
|
||||||
@@ -10,21 +8,25 @@ import (
|
|||||||
|
|
||||||
// ShearRemoteFromClient removes the target file or directory from the local system and calls the server to remove it remotely and add it to the deletions list.
|
// ShearRemoteFromClient removes the target file or directory from the local system and calls the server to remove it remotely and add it to the deletions list.
|
||||||
// It can safely be called in offline mode, as well, so this is the intended interface for shearing (ShearLocal should only be used directly by the server binary).
|
// It can safely be called in offline mode, as well, so this is the intended interface for shearing (ShearLocal should only be used directly by the server binary).
|
||||||
func ShearRemoteFromClient(targetLocationIncomplete string) {
|
func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) {
|
||||||
deviceID := ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client
|
deviceID, isDir := ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client
|
||||||
|
|
||||||
if deviceID != "" { // ensure a device ID exists (online mode)
|
if !forceOffline && deviceID != "" { // ensure a device ID exists (online mode)
|
||||||
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
||||||
sshClient, _, _ := GetSSHClient(false)
|
sshClient, _, _ := GetSSHClient(false)
|
||||||
|
|
||||||
|
// ensure targetLocationIncomplete ends with a slash if it is a directory (for clarity in shear message)
|
||||||
|
if isDir && !strings.HasSuffix(targetLocationIncomplete, "/") {
|
||||||
|
targetLocationIncomplete += "/"
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
GetSSHOutput(sshClient, "libmuttonserver shear", deviceID+"\n"+strings.ReplaceAll(targetLocationIncomplete, core.PathSeparator, core.FSPath))
|
GetSSHOutput(sshClient, "libmuttonserver shear", deviceID+"\n"+strings.ReplaceAll(targetLocationIncomplete, core.PathSeparator, core.FSPath))
|
||||||
|
|
||||||
// close the SSH client
|
// close the SSH client
|
||||||
err := sshClient.Close()
|
err := sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||||
os.Exit(core.ErrorServerConnection)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,11 +35,11 @@ func ShearRemoteFromClient(targetLocationIncomplete string) {
|
|||||||
|
|
||||||
// RenameRemoteFromClient renames oldLocationIncomplete to newLocationIncomplete on the local system and calls the server to perform the rename remotely and add the old target to the deletions list.
|
// RenameRemoteFromClient renames oldLocationIncomplete to newLocationIncomplete on the local system and calls the server to perform the rename remotely and add the old target to the deletions list.
|
||||||
// It can safely be called in offline mode, as well, so this is the intended interface for renaming (RenameLocal should only be used directly by the server binary).
|
// It can safely be called in offline mode, as well, so this is the intended interface for renaming (RenameLocal should only be used directly by the server binary).
|
||||||
func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string) {
|
func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string, forceOffline bool) {
|
||||||
RenameLocal(oldLocationIncomplete, newLocationIncomplete, false) // move the target on the local system
|
RenameLocal(oldLocationIncomplete, newLocationIncomplete, false) // move the target on the local system
|
||||||
|
|
||||||
deviceIDList := core.GenDeviceIDList(true)
|
deviceIDList := core.GenDeviceIDList(true)
|
||||||
if len(*deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
if !forceOffline && len(*deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
||||||
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
||||||
sshClient, _, _ := GetSSHClient(false)
|
sshClient, _, _ := GetSSHClient(false)
|
||||||
|
|
||||||
@@ -50,8 +52,7 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string)
|
|||||||
// close the SSH client
|
// close the SSH client
|
||||||
err := sshClient.Close()
|
err := sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||||
os.Exit(core.ErrorServerConnection)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,11 +61,11 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete 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.
|
||||||
// It 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).
|
// It 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) {
|
func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline bool) {
|
||||||
AddFolderLocal(targetLocationIncomplete) // add the folder on the local system
|
AddFolderLocal(targetLocationIncomplete) // add the folder on the local system
|
||||||
|
|
||||||
deviceIDList := core.GenDeviceIDList(true)
|
deviceIDList := core.GenDeviceIDList(true)
|
||||||
if len(*deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
if !forceOffline && len(*deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
||||||
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
||||||
sshClient, _, _ := GetSSHClient(false)
|
sshClient, _, _ := GetSSHClient(false)
|
||||||
|
|
||||||
@@ -74,8 +75,7 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string) {
|
|||||||
// close the SSH client
|
// close the SSH client
|
||||||
err := sshClient.Close()
|
err := sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
core.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||||
os.Exit(core.ErrorServerConnection)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -17,8 +17,7 @@ func GetRemoteDataFromServer(clientDeviceID string) {
|
|||||||
modList := getModTimes(entryList)
|
modList := getModTimes(entryList)
|
||||||
deletionsList, err := os.ReadDir(core.ConfigDir + core.PathSeparator + "deletions")
|
deletionsList, err := os.ReadDir(core.ConfigDir + core.PathSeparator + "deletions")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(core.AnsiError+"Failed to read the deletions directory:", err.Error()+core.AnsiReset)
|
core.PrintError("Failed to read the deletions directory: "+err.Error(), core.ErrorRead, true)
|
||||||
os.Exit(core.ErrorRead)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// print the current UNIX timestamp to stdout
|
// print the current UNIX timestamp to stdout
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
## Known Bugs - libmutton
|
## Known Bugs - libmutton
|
||||||
- On Windows, GPG is sometimes (seems unpredictable) incredibly slow to start (often after a reboot), leading to many operations seemingly hanging
|
- On Windows, GPG is sometimes (seems unpredictable) incredibly slow to start (often after a reboot), leading to many operations seemingly hanging
|
||||||
- **This will be addressed** in the migration off of GPG that will take place before v1.0.0
|
- **This will be addressed** in the migration off of GPG that will take place before v1.0.0
|
||||||
- Password-protected SSH identity files currently only prompt for password entry in the CLI, and thus they are not yet supported in GUI/TUI implementations
|
|
||||||
|
|||||||
+7
-6
@@ -11,14 +11,15 @@ If any functionality in these two packages proves to be difficult to implement i
|
|||||||
Custom build tags can (and sometimes must) be used to achieve desired results.
|
Custom build tags can (and sometimes must) be used to achieve desired results.
|
||||||
|
|
||||||
These are as follows:
|
These are as follows:
|
||||||
- `returnOnExit`: If making an interactive interface (GUI/TUI/interactive CLI), you probably need to use this build tag. Without it, your entire program will exit after any given operation is completed. This behavior is only desired for non-interactive CLI implementations, such as MUTN. Currently, errors will result in the program exiting **even with this build tag**. This may be changed in the future (under evaluation).
|
- `interactive`: If making an interactive interface (GUI/TUI/interactive CLI), you probably need to use this build tag. Without it, your entire program will exit after any given operation is completed. This behavior is only desired for non-interactive CLI implementations, such as MUTN. Currently, most errors will result in the program exiting **even with this build tag**. Specific types of errors (such as config parsing/SSH dialing errors) have been made exempt from this behavior.
|
||||||
- `wsl`: Allows creating a Linux binary that can interact with the Windows clipboard (for WSL)
|
- `wsl`: Allows creating a Linux binary that can interact with the Windows clipboard (for WSL)
|
||||||
- `termux`: Allows creating a Linux binary that can interact with the Termux clipboard (for Android)
|
- `termux`: Allows creating an Android binary that can interact with the Termux clipboard (for Android)
|
||||||
|
|
||||||
## Required Arguments
|
## Required Global Variable Manipulation
|
||||||
libmutton-based password manager clients should accept at least one specific required argument, as well as another recommended one:
|
libmutton provides a `PassphraseInputFunction` global variable that all clients must set to support passphrase-protected SSH identity files. This approach allows for different types of clients (CLI, GUI, TUI) to prompt for the passphrase in the most appropriate way.
|
||||||
- `clipclear`: This argument is required for correct functionality. In order to clear the clipboard on a timer, libmutton-based password managers call another instance of their executable with the `clipclear` argument (e.g. `mutn clipclear`) with the intended clipboard contents provided via STDIN. If after 30 seconds the clipboard contents have not changed, they are cleared. Please accept a `clipclear` argument that is processed before the launch of any interactive interface. All this argument needs to do is call `core.ClipClearArgument()`.
|
|
||||||
- `init`: This argument is optional, but recommended for CLI interfaces. Some error messages request the user to use the `init` argument to fix configuration issues. If the argument does not exist, this may confuse the user.
|
## Required Argument (clipclear)
|
||||||
|
The `clipclear` argument should be accepted by all non-interactive CLI libmutton implementations (not required for interactive GUI/TUI implementations). In order to clear the clipboard on a timer, non-interactive libmutton-based password managers call another instance of their executable with the `clipclear` argument (e.g. `mutn clipclear`) with the intended clipboard contents provided via STDIN. If after 30 seconds the clipboard contents have not changed, they are cleared. Please accept a `clipclear` argument that calls `core.ClipClearArgument()`.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
libmutton-based password manager clients should all share the same INI configuration file.
|
libmutton-based password manager clients should all share the same INI configuration file.
|
||||||
|
|||||||
Reference in New Issue
Block a user