mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-28 12:56:31 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f98e0b577 | ||
|
|
8f23589459 | ||
|
|
1dff1f463a | ||
|
|
4ed816cb11 | ||
|
|
451d695649 | ||
|
|
259d9a3309 | ||
|
|
51bb67f315 | ||
|
|
0f90e82514 | ||
|
|
44924737ab | ||
|
|
17223758b1 | ||
|
|
f40699076d | ||
|
|
a19a4aa1d5 | ||
|
|
456d20256c | ||
|
|
da95ede807 | ||
|
|
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 | ||
|
|
1db9467446 | ||
|
|
3e966ade11 | ||
|
|
dd6bde5e4d | ||
|
|
91fdeb53d5 | ||
|
|
d613ace494 | ||
|
|
003c373cc8 | ||
|
|
cdb58e9984 | ||
|
|
4f923e7272 | ||
|
|
fcde9689c8 | ||
|
|
ab3f6be41b | ||
|
|
1fb2834844 |
@@ -4,3 +4,4 @@
|
||||
/libmuttonserver.exe
|
||||
/main
|
||||
/main.exe
|
||||
1output
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -8,19 +8,17 @@ 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.
|
||||
>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).
|
||||
|
||||
# Roadmap
|
||||
#### Release v0.3.0
|
||||
- [ ] Swap to native (cascade) encryption (custom)
|
||||
- [ ] Implement "netpin" (quick-unlock) with new encryption
|
||||
#### Release v0.4.0
|
||||
- [ ] Swap to native (cascade) encryption (custom)
|
||||
- [ ] Add refresh/re-encrypt functionality
|
||||
#### Release v0.5.0
|
||||
- [ ] Implement "netpin" (quick-unlock) with new encryption
|
||||
- [ ] Password aging support
|
||||
- [ ] Append UNIX timestamp to entry names
|
||||
- [ ] Add yellow/red dot indicators to entry list readout for when passwords should be changed
|
||||
#### Release v0.5.0
|
||||
- [ ] Add refresh/re-encrypt functionality
|
||||
#### Release v1.0.0
|
||||
- [ ] Create packaging scripts (libmuttonserver)
|
||||
- [ ] Stable source PKGBUILD
|
||||
@@ -29,4 +27,4 @@ See the [developer guide](https://github.com/rwinkhart/libmutton/blob/main/wiki/
|
||||
- [ ] Fedora
|
||||
- [ ] FreeBSD
|
||||
- [ ] Windows installer
|
||||
- Hunt for polishing opportunities and bugs
|
||||
- Perform extensive testing
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
gofmt -l -w -s ./core/*.go ./sync/*.go ./libmuttonserver.go
|
||||
git commit -am "$1"
|
||||
git push
|
||||
+8
-5
@@ -4,16 +4,19 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type ByteInputFetcher func(prompt string) []byte
|
||||
|
||||
var (
|
||||
Home, _ = os.UserHomeDir()
|
||||
PassphraseInputFunction ByteInputFetcher // Clients should set this to a function that fetches hidden input from the user
|
||||
Home, _ = os.UserHomeDir()
|
||||
)
|
||||
|
||||
const (
|
||||
LibmuttonVersion = "0.2.2" // 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.1" // 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
|
||||
FSPath = "\u259e" // ▞ path separator
|
||||
FSMisc = "\u259f" // ▟ misc. field separator (if \u259d is already used)
|
||||
FSSpace = "\u259d" // ▝ Space/list separator
|
||||
FSPath = "\u259e" // ▞ Path separator
|
||||
FSMisc = "\u259f" // ▟ Misc. field separator (if \u259d is already used)
|
||||
|
||||
AnsiError = "\033[38;5;9m"
|
||||
AnsiReset = "\033[0m"
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
package core
|
||||
|
||||
var EntryRoot = Home + "/.local/share/libmutton" // path to libmutton entry directory
|
||||
var ConfigDir = Home + "/.config/libmutton" // path to libmutton configuration directory
|
||||
var ConfigPath = ConfigDir + "/libmutton.ini" // path to libmutton configuration file
|
||||
var (
|
||||
EntryRoot = Home + "/.local/share/libmutton" // Path to libmutton entry directory
|
||||
ConfigDir = Home + "/.config/libmutton" // Path to libmutton configuration directory
|
||||
ConfigPath = ConfigDir + "/libmutton.ini" // Path to libmutton configuration file
|
||||
)
|
||||
|
||||
const (
|
||||
PathSeparator = "/" // platform-specific path separator
|
||||
IsWindows = false // platform indicator
|
||||
PathSeparator = "/" // Platform-specific path separator
|
||||
IsWindows = false // Platform indicator
|
||||
)
|
||||
|
||||
// enableVirtualTerminalProcessing is a dummy function on UNIX-like systems (only needed on Windows).
|
||||
|
||||
+7
-5
@@ -7,13 +7,15 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
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 ConfigPath = ConfigDir + "\\libmutton.ini" // path to libmutton configuration file
|
||||
var (
|
||||
EntryRoot = Home + "\\AppData\\Local\\libmutton\\entries" // Path to libmutton entry directory
|
||||
ConfigDir = Home + "\\AppData\\Local\\libmutton\\config" // Path to libmutton configuration directory
|
||||
ConfigPath = ConfigDir + "\\libmutton.ini" // Path to libmutton configuration file
|
||||
)
|
||||
|
||||
const (
|
||||
PathSeparator = "\\" // platform-specific path separator
|
||||
IsWindows = true // platform indicator
|
||||
PathSeparator = "\\" // Platform-specific path separator
|
||||
IsWindows = true // Platform indicator
|
||||
)
|
||||
|
||||
// enableVirtualTerminalProcessing ensures ANSI escape sequences are interpreted properly on Windows.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//go:build android && !termux
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
clearClipboard := func() {
|
||||
clipboard.Write(clipboard.FmtText, []byte(""))
|
||||
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 := clipboard.Read(clipboard.FmtText)
|
||||
|
||||
if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||
clearClipboard()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build !android || termux
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
+27
-16
@@ -13,17 +13,18 @@ import (
|
||||
func loadConfig() *ini.File {
|
||||
cfg, err := ini.Load(ConfigPath)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to load libmutton.ini:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorRead)
|
||||
PrintError("Failed to load libmutton.ini: "+err.Error(), ErrorRead, true)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Returns: config (slice of values for the specified keys).
|
||||
func ParseConfig(valuesRequested [][2]string, missingValueError string) []string {
|
||||
// Returns: config (slice of values for the specified keys),
|
||||
// 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()
|
||||
|
||||
var config []string
|
||||
@@ -35,19 +36,21 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) []string
|
||||
if value == "" {
|
||||
switch missingValueError {
|
||||
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":
|
||||
Exit(0)
|
||||
Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
|
||||
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)
|
||||
}
|
||||
|
||||
return config
|
||||
return config, err
|
||||
}
|
||||
|
||||
// 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")
|
||||
if err != nil {
|
||||
if errorOnFail {
|
||||
fmt.Println(AnsiError+"Failed to read the devices directory:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorRead)
|
||||
PrintError("Failed to read the devices directory: "+err.Error(), ErrorRead, true)
|
||||
} else {
|
||||
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.
|
||||
// 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) {
|
||||
// Requires: valuesToWrite (a slice of length 3 arrays each containing a section, a key name, and a value),
|
||||
// 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
|
||||
|
||||
if append {
|
||||
@@ -94,10 +98,17 @@ func WriteConfig(valuesToWrite [][3]string, append bool) {
|
||||
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
|
||||
err := cfg.SaveTo(ConfigPath)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to save libmutton.ini:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorWrite)
|
||||
PrintError("Failed to save libmutton.ini: "+err.Error(), ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-13
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// 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 {
|
||||
|
||||
decryptedEntry := DecryptGPG(targetLocation)
|
||||
@@ -23,8 +23,7 @@ func CopyArgument(executableName, targetLocation string, field int) {
|
||||
|
||||
// ensure field is not empty
|
||||
if decryptedEntry[field] == "" {
|
||||
fmt.Println(AnsiError + "Field is empty" + AnsiReset)
|
||||
os.Exit(ErrorTargetNotFound)
|
||||
PrintError("Field is empty", ErrorTargetNotFound, true)
|
||||
}
|
||||
|
||||
if field != 2 {
|
||||
@@ -42,30 +41,29 @@ 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")
|
||||
|
||||
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()
|
||||
copyField("", GenTOTP(secret, currentTime, forSteam))
|
||||
copyString(true, GenTOTP(secret, currentTime, forSteam))
|
||||
// sleep until next 30-second interval
|
||||
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println(AnsiError + "Field does not exist in entry" + AnsiReset)
|
||||
os.Exit(ErrorTargetNotFound)
|
||||
PrintError("Field does not exist in entry", ErrorTargetNotFound, true)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// read previous clipboard contents from stdin
|
||||
clipScanner := bufio.NewScanner(os.Stdin)
|
||||
if clipScanner.Scan() {
|
||||
oldContents := clipScanner.Text()
|
||||
clipClear(oldContents)
|
||||
assignedContents := clipScanner.Text()
|
||||
clipClearProcess(assignedContents)
|
||||
} 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)
|
||||
}
|
||||
@@ -83,8 +81,7 @@ func GenTOTP(secret string, time time.Time, forSteam bool) string {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Error generating TOTP code" + AnsiReset)
|
||||
os.Exit(ErrorOther)
|
||||
PrintError("Error generating TOTP code", ErrorOther, true)
|
||||
}
|
||||
|
||||
return totpToken
|
||||
|
||||
+11
-40
@@ -3,55 +3,26 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// copyField copies a field from an entry to the clipboard.
|
||||
func copyField(executableName, copySubject string) {
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
cmd := exec.Command("pbcopy")
|
||||
writeToStdin(cmd, copySubject)
|
||||
WriteToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to copy to clipboard:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorClipboard)
|
||||
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||
}
|
||||
|
||||
// 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
|
||||
if !continuous {
|
||||
LaunchClipClearProcess(copySubject)
|
||||
}
|
||||
}
|
||||
|
||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds.
|
||||
func clipClear(oldContents string) {
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
cmd := exec.Command("pbpaste")
|
||||
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)
|
||||
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||
cmdClear := exec.Command("pbcopy")
|
||||
WriteToStdin(cmdClear, "")
|
||||
return exec.Command("pbpaste"), cmdClear
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build android && !termux
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
// TODO Investigate background clipboard clearing and on-app-close clipboard clearing for Android
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
clipboard.Write(clipboard.FmtText, []byte(copySubject))
|
||||
|
||||
if !continuous {
|
||||
LaunchClipClearProcess(copySubject)
|
||||
}
|
||||
}
|
||||
+12
-41
@@ -1,57 +1,28 @@
|
||||
//go:build linux && termux
|
||||
//go:build android && termux
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// copyField copies a field from an entry to the clipboard.
|
||||
func copyField(executableName, copySubject string) {
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
cmd := exec.Command("termux-clipboard-set")
|
||||
writeToStdin(cmd, copySubject)
|
||||
WriteToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to copy to clipboard:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorClipboard)
|
||||
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||
}
|
||||
|
||||
// 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
|
||||
if !continuous {
|
||||
LaunchClipClearProcess(copySubject)
|
||||
}
|
||||
}
|
||||
|
||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds.
|
||||
func clipClear(oldContents string) {
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
cmd := exec.Command("termux-clipboard-get")
|
||||
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)
|
||||
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||
cmdClear := exec.Command("termux-clipboard-set")
|
||||
WriteToStdin(cmdClear, "")
|
||||
return exec.Command("termux-clipboard-get"), cmdClear
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
|
||||
cmd = exec.Command("xclip", "-sel", "c")
|
||||
} 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// copyField copies a field from an entry to the clipboard.
|
||||
func copyField(executableName, copySubject string) {
|
||||
// copyString copies a string to the clipboard.
|
||||
func copyString(continuous bool, copySubject string) {
|
||||
cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to copy to clipboard:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorClipboard)
|
||||
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||
}
|
||||
|
||||
// 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
|
||||
if !continuous {
|
||||
LaunchClipClearProcess(copySubject)
|
||||
}
|
||||
}
|
||||
|
||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds.
|
||||
func clipClear(oldContents string) {
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
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)
|
||||
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||
return exec.Command("powershell.exe", "-c", "Get-Clipboard"), exec.Command("powershell.exe", "-c", "Set-Clipboard")
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
//go:build !returnOnExit
|
||||
//go:build !interactive
|
||||
|
||||
package core
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
//go:build returnOnExit
|
||||
//go:build interactive
|
||||
|
||||
package core
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
+6
-10
@@ -1,8 +1,6 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
@@ -18,22 +16,20 @@ func DecryptGPG(targetLocation string) []string {
|
||||
enableVirtualTerminalProcessing()
|
||||
|
||||
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)
|
||||
os.Exit(ErrorDecryption)
|
||||
PrintError("Failed to decrypt \""+targetLocation+"\" - Ensure it is a valid GPG-encrypted file and that you entered your passphrase correctly", ErrorDecryption, true)
|
||||
}
|
||||
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.
|
||||
func EncryptGPG(input []string) []byte {
|
||||
cmd := exec.Command("gpg", "-q", "-r", ParseConfig([][2]string{{"LIBMUTTON", "gpgID"}}, "")[0], "-e")
|
||||
writeToStdin(cmd, strings.Join(input, "\n"))
|
||||
gpgCfg, _ := ParseConfig([][2]string{{"LIBMUTTON", "gpgID"}}, "")
|
||||
cmd := exec.Command("gpg", "-q", "-r", gpgCfg[0], "-e")
|
||||
WriteToStdin(cmd, strings.Join(input, "\n"))
|
||||
encryptedBytes, err := cmd.Output()
|
||||
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)
|
||||
os.Exit(ErrorEncryption)
|
||||
PrintError("Failed to encrypt data - Ensure that you have a valid GPG ID set in libmutton.ini", ErrorEncryption, true)
|
||||
}
|
||||
return encryptedBytes
|
||||
}
|
||||
|
||||
+18
-16
@@ -1,7 +1,6 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
@@ -45,8 +44,7 @@ func GpgKeyGen() string {
|
||||
cmd.Stdin = os.Stdin
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to generate GPG key:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorOther)
|
||||
PrintError("Failed to generate GPG key: "+err.Error(), ErrorOther, true)
|
||||
}
|
||||
|
||||
return "libmutton-" + unixTime + " (gpg-libmutton) <github.com/rwinkhart/libmutton>"
|
||||
@@ -58,18 +56,11 @@ func DirInit(preserveOldConfigDir bool) string {
|
||||
// create EntryRoot
|
||||
err := os.MkdirAll(EntryRoot, 0700)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to create \""+EntryRoot+"\":", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorWrite)
|
||||
PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), ErrorWrite, true)
|
||||
}
|
||||
|
||||
// get old device ID before its potential removal
|
||||
oldDeviceIDList := GenDeviceIDList(false) // errorOnFail is false so that nil is received when the devices directory does not exist
|
||||
var oldDeviceID string
|
||||
if oldDeviceIDList != nil && len(*oldDeviceIDList) > 0 { // ensure not derferencing nil, which occurs when the devices directory does not exist
|
||||
oldDeviceID = (*oldDeviceIDList)[0].Name()
|
||||
} else {
|
||||
oldDeviceID = FSMisc // indicates to server that no device ID is being replaced
|
||||
}
|
||||
oldDeviceID := GetCurrentDeviceID()
|
||||
|
||||
// remove existing config directory (if it exists and not in append mode)
|
||||
if !preserveOldConfigDir {
|
||||
@@ -77,8 +68,7 @@ func DirInit(preserveOldConfigDir bool) string {
|
||||
if isAccessible {
|
||||
err = os.RemoveAll(ConfigDir)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to remove existing config directory:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorWrite)
|
||||
PrintError("Failed to remove existing config directory: "+err.Error(), ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,9 +76,21 @@ func DirInit(preserveOldConfigDir bool) string {
|
||||
// create config directory w/devices subdirectory
|
||||
err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to create \""+ConfigDir+"\":", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorWrite)
|
||||
PrintError("Failed to create \""+ConfigDir+"\": "+err.Error(), ErrorWrite, true)
|
||||
}
|
||||
|
||||
return oldDeviceID
|
||||
}
|
||||
|
||||
// GetOldDeviceID returns the current device ID or
|
||||
// FSMisc if there is no device ID (e.g. first run).
|
||||
func GetCurrentDeviceID() string {
|
||||
deviceIDList := GenDeviceIDList(false) // errorOnFail is false so that nil is received when the devices directory does not exist
|
||||
var deviceID string
|
||||
if deviceIDList != nil && len(*deviceIDList) > 0 { // ensure not derferencing nil, which occurs when the devices directory does not exist
|
||||
deviceID = (*deviceIDList)[0].Name()
|
||||
} else {
|
||||
deviceID = FSMisc // indicates to server that no device ID is being replaced
|
||||
}
|
||||
return deviceID
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+94
-34
@@ -18,50 +18,38 @@ func TargetIsFile(targetLocation string, errorOnFail bool, failCondition uint8)
|
||||
targetInfo, err := os.Stat(targetLocation)
|
||||
if err != nil {
|
||||
if errorOnFail {
|
||||
fmt.Println(AnsiError + "Failed to access \"" + targetLocation + "\" - Ensure it exists and has the correct permissions" + AnsiReset)
|
||||
os.Exit(ErrorTargetNotFound)
|
||||
PrintError("Failed to access \""+targetLocation+"\" - Ensure it exists and has the correct permissions", ErrorTargetNotFound, true)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
if targetInfo.IsDir() {
|
||||
if errorOnFail && failCondition == 2 {
|
||||
fmt.Println(AnsiError + "\"" + targetLocation + "\" is a directory" + AnsiReset)
|
||||
os.Exit(ErrorTargetWrongType)
|
||||
PrintError("\""+targetLocation+"\" is a directory", ErrorTargetWrongType, true)
|
||||
}
|
||||
return false, true
|
||||
} else {
|
||||
if errorOnFail && failCondition == 1 {
|
||||
fmt.Println(AnsiError + "\"" + targetLocation + "\" is a file" + AnsiReset)
|
||||
os.Exit(ErrorTargetWrongType)
|
||||
PrintError("\""+targetLocation+"\" is a file", ErrorTargetWrongType, true)
|
||||
}
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
|
||||
// WriteEntry writes entryData to an encrypted file at targetLocation.
|
||||
func WriteEntry(targetLocation string, entryData []string, verifyEntryDoesNotExist bool) {
|
||||
if verifyEntryDoesNotExist {
|
||||
_, isAccessible := TargetIsFile(targetLocation, false, 0)
|
||||
if isAccessible {
|
||||
fmt.Println(AnsiError + "Target location already exists" + AnsiReset)
|
||||
os.Exit(ErrorTargetExists)
|
||||
}
|
||||
}
|
||||
|
||||
func WriteEntry(targetLocation string, entryData []string) {
|
||||
encryptedBytes := EncryptGPG(entryData)
|
||||
err := os.WriteFile(targetLocation, encryptedBytes, 0600)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to write to file:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorWrite)
|
||||
PrintError("Failed to write to file: "+err.Error(), ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
|
||||
// writeToStdin is a utility function that writes a string to a command's stdin.
|
||||
func writeToStdin(cmd *exec.Cmd, input string) {
|
||||
// WriteToStdin is a utility function that writes a string to a command's stdin.
|
||||
// TODO unexport (import?) after migration off of GPG
|
||||
func WriteToStdin(cmd *exec.Cmd, input string) {
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to access stdin for system command:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorOther)
|
||||
PrintError("Failed to access stdin for system command: "+err.Error(), ErrorOther, true)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -76,8 +64,7 @@ func writeToStdin(cmd *exec.Cmd, input string) {
|
||||
func CreateTempFile() *os.File {
|
||||
tempFile, err := os.CreateTemp("", "*.markdown")
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError+"Failed to create temporary file:", err.Error()+AnsiReset)
|
||||
os.Exit(ErrorWrite)
|
||||
PrintError("Failed to create temporary file: "+err.Error(), ErrorWrite, true)
|
||||
}
|
||||
return tempFile
|
||||
}
|
||||
@@ -89,26 +76,81 @@ func RemoveTrailingEmptyStrings(slice []string) []string {
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EntryAddPrecheck ensures the directory meant to contain a new
|
||||
// entry exists and that the target entry location is not already used.
|
||||
// Returns: statusCode (0 = success, 1 = target location already exists, 2 = containing directory is invalid).
|
||||
func EntryAddPrecheck(targetLocation string) uint8 {
|
||||
// ensure target location does not already exist
|
||||
_, isAccessible := TargetIsFile(targetLocation, false, 0)
|
||||
if isAccessible {
|
||||
PrintError("Target location already exists", ErrorTargetExists, false)
|
||||
return 1 // inform interactive clients that the target location already exists
|
||||
}
|
||||
// ensure target containing directory exists and is a directory (not a file)
|
||||
containingDir := targetLocation[:strings.LastIndex(targetLocation, PathSeparator)]
|
||||
isFile, isAccisAccessible := TargetIsFile(containingDir, false, 1)
|
||||
if isFile || !isAccisAccessible {
|
||||
PrintError("\""+containingDir+"\" is not a valid containing directory", ErrorTargetWrongType, false)
|
||||
return 2 // inform interactive clients that the containing directory is invalid
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// StringGen generates a random string of a specified length and complexity.
|
||||
// Requires: complexity (minimum percentage of special characters to be returned in the generated string; only impacts complex strings),
|
||||
// safeForFileName: (if true, the generated string will only contain special characters that are safe for file names; only impacts complex strings).
|
||||
func StringGen(length int, complex bool, complexity float64, safeForFileName bool) string {
|
||||
// Requires: complexity (minimum percentage of special characters to be returned in the generated string; set to 0 to generate a simple string),
|
||||
// complexCharsetLevel (1 = safe for filenames, 2 = safe for most password entries, 3 = safe only for well-made password entries)
|
||||
func StringGen(length int, complexity float64, complexCharsetLevel uint8) string {
|
||||
var actualSpecialChars int // track the number of special characters in the generated string
|
||||
var minSpecialChars int // track the minimum number of special characters to accept
|
||||
var extendedCharset string // additions to character set used for complex strings
|
||||
|
||||
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // default character set used for all strings
|
||||
const extendedCharsetFiles = "!#$%&'()+,-.;=@[]^_`{}~" // additional special characters for complex strings (safe in file names)
|
||||
const extendedCharsetPassword = "\"*:><?/\\|" // additional special characters for complex strings (NOT safe in file names)
|
||||
if complex {
|
||||
const extendedCharsetFiles = "!#$%&+,-.;=@_~^()[]{}`'" // additional special characters for complex strings (safe in file names)
|
||||
const extendedCharsetMostPassword = "*:><?|" // additional special characters for complex strings (NOT safe in file names)
|
||||
const extendedCharsetSpecialPassword = "\"/\\" // additional special characters for complex strings (NOT safe in file names)
|
||||
|
||||
if complexity > 0 {
|
||||
minSpecialChars = int(math.Round(float64(length) * complexity)) // determine minimum number of special characters to accept
|
||||
if !safeForFileName {
|
||||
extendedCharset = extendedCharsetFiles + extendedCharsetPassword
|
||||
} else {
|
||||
switch complexCharsetLevel {
|
||||
case 1:
|
||||
extendedCharset = extendedCharsetFiles
|
||||
case 2:
|
||||
extendedCharset = extendedCharsetMostPassword + extendedCharsetFiles[:len(extendedCharsetFiles)-9]
|
||||
case 3:
|
||||
extendedCharset = extendedCharsetFiles + extendedCharsetMostPassword + extendedCharsetSpecialPassword
|
||||
}
|
||||
charset += extendedCharset
|
||||
}
|
||||
@@ -123,7 +165,7 @@ func StringGen(length int, complex bool, complexity float64, safeForFileName boo
|
||||
}
|
||||
|
||||
// return early if the string is not complex
|
||||
if !complex {
|
||||
if complexity == 0 {
|
||||
return string(result)
|
||||
}
|
||||
|
||||
@@ -154,3 +196,21 @@ func EntryIsNotEmpty(entryData []string) bool {
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,38 @@
|
||||
**libmutton v0.2.4**
|
||||
December 13, 2024
|
||||
|
||||
## IMPORTANT
|
||||
- This release includes a dependency bump to address a high-severity CVE (CVE-2024-45337) - Please update as soon as possible!
|
||||
|
||||
## Changes
|
||||
- (91fdeb53d5b58fe0a554ef29c1384a50b1a9ec33) The MIME type of data copied to the clipboard on UNIX-like systems (X11 and Wayland) is now statically set to "text/plain" to avoid potential issues with type inference
|
||||
|
||||
## Dependencies
|
||||
- Bumps (direct and indirect)
|
||||
- Go: v1.23.2 => v1.23.4
|
||||
- github.com/pkg/sftp: v1.13.6 => v1.13.7
|
||||
- golang.org/x/crypto: v0.28.0 => v0.31.0
|
||||
- Addresses CVE-2024-45337
|
||||
- golang.org/x/sys: v0.26.0 => v0.28.0
|
||||
- golang.org/x/term: v0.25.0 => v0.27.0
|
||||
|
||||
---
|
||||
|
||||
**libmutton v0.2.3**
|
||||
October 19, 2024
|
||||
|
||||
## Fixes
|
||||
- (1fb283484469192fc7c954f14d6f2be5234d7c6b) Fixed renaming/moving an entry/folder to an invalid path still having clients request the operation on the server
|
||||
|
||||
## Dependencies
|
||||
- Bumps (direct and indirect)
|
||||
- Go: v1.22.6 => v1.23.2
|
||||
- golang.org/x/crypto: v0.26.0 => v0.28.0
|
||||
- golang.org/x/sys: v0.24.0 => v0.26.0
|
||||
- golang.org/x/term: v0.23.0 => v0.25.0
|
||||
|
||||
---
|
||||
|
||||
**libmutton v0.2.2**
|
||||
August 20, 2024
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
**libmutton v0.3.1**
|
||||
April 21, 2025
|
||||
|
||||
## Features
|
||||
- (da95ede80704e667f75372e2142de4cef1dd42da) Added `EntryAddPrecheck()` utility function for ensuring the target locations for new entries are valid
|
||||
- (17223758b11eae301106f398fe2a8815a04f2d62) (44924737ab0830ad49e90c1d1930b05bba307284) (0f90e8251413af2be73f56d071c14af6c00cdb79) Added initial native Android support
|
||||
- (51bb67f3150e70c2c8e8dc3cc72de5666d7c058a) Split device ID retrieval into dedicated exported function, `GetCurrentDeviceID`
|
||||
- (4ed816cb112686d995b1cb7ae8f5df990117ae4d) Allow more control over string generation to improve generated password compatibility
|
||||
|
||||
## Fixes
|
||||
- (51bb67f3150e70c2c8e8dc3cc72de5666d7c058a) Fixed clients having multiple device IDs after failed registrations
|
||||
- (451d695649c659a510a117254a9311fe621082b1) Fixed inability to add a folder on the server if the folder was already created on a client that failed to sync
|
||||
|
||||
## Dependencies
|
||||
- Bumps (direct and indirect)
|
||||
- Go: v1.24.0 => v1.24.2
|
||||
- github.com/pkg/sftp: v1.13.7 => v1.13.9
|
||||
- golang.org/x/crypto: v0.34.0 => v0.37.0
|
||||
- golang.org/x/sys: v0.30.0 => v0.32.0
|
||||
- New
|
||||
- Android builds only
|
||||
- golang.design/x/clipboard v0.7.0
|
||||
- golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
- golang.org/x/image v0.26.0
|
||||
- golang.org/x/mobile v0.0.0-20250408133729-978277e7eaf7
|
||||
|
||||
---
|
||||
|
||||
**libmutton v0.3.0**
|
||||
February 22, 2025
|
||||
|
||||
## Features
|
||||
- (f9aa2fc374b77fc8325d0c7644ffc9c96169c3c6) (d2034b458b702b8f1664bc188c1840040ec0f704) (81b78068a75a23c73be607cbe6d4dc8b1539f831) (e9d0c3704362c8963e63a6c3b70d036f3e6c2493) `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` can be used to decrease the overall size of client binaries through 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,21 @@
|
||||
module github.com/rwinkhart/libmutton
|
||||
|
||||
go 1.22.6
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pkg/sftp v1.13.9
|
||||
github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/term v0.23.0
|
||||
golang.design/x/clipboard v0.7.0 // only for Android builds
|
||||
golang.org/x/crypto v0.37.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 // indirect; only for Android builds
|
||||
golang.org/x/image v0.26.0 // indirect; only for Android builds
|
||||
golang.org/x/mobile v0.0.0-20250408133729-978277e7eaf7 // indirect; only for Android builds
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
)
|
||||
|
||||
@@ -6,10 +6,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727 h1:1RkPJqfzrncAuh9xgoslr9OplZskm+VRA9lkucHPQZ4=
|
||||
github.com/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727/go.mod h1:wRAWHbTlpt0C4kwnKoa42L2Phrv6uIq+c50P2uKpb7I=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
|
||||
github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481 h1:FkxbO331O7mS5EJkP+MCi0o2gswh/Aezs+//NmefrR8=
|
||||
@@ -21,38 +22,81 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo=
|
||||
golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E=
|
||||
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.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80=
|
||||
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/mobile v0.0.0-20250408133729-978277e7eaf7 h1:8MGTx39304caZ/OMsjPfuxUoDGI2tRas92F5x97tIYc=
|
||||
golang.org/x/mobile v0.0.0-20250408133729-978277e7eaf7/go.mod h1:ftACcHgQ7vaOnQbHOHvXt9Y6bEPHrs5Ovk67ClwrPJA=
|
||||
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
|
||||
+2
-4
@@ -77,7 +77,7 @@ func main() {
|
||||
}
|
||||
|
||||
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 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,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.` + "\n\n---------------------------------------------------------")
|
||||
fmt.Print(ansiBold + "\n\n libmuttonserver" + core.AnsiReset + " Version " + core.LibmuttonVersion + `
|
||||
|
||||
Copyright (c) 2024 Randall Winkhart` + "\n\n")
|
||||
fmt.Print(ansiBold + "\n\n libmuttonserver" + core.AnsiReset + " Version " + core.LibmuttonVersion + "\n\n Copyright (c) 2024-2025: Randall Winkhart" + "\n\n")
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ package sync
|
||||
|
||||
import "github.com/rwinkhart/libmutton/core"
|
||||
|
||||
var rootLength = len(core.EntryRoot) // length of core.EntryRoot string
|
||||
var RootLength = len(core.EntryRoot) // length of core.EntryRoot string
|
||||
|
||||
+55
-65
@@ -27,11 +27,11 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
var sshUserConfig []string
|
||||
var missingValueError string
|
||||
if manualSync {
|
||||
missingValueError = joinErrorWithEXE("SSH settings not configured - Run \"", " init\" to configure")
|
||||
missingValueError = "SSH settings not fully configured"
|
||||
} 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 isWindows bool
|
||||
@@ -53,8 +53,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
case 6:
|
||||
isWindows, err = strconv.ParseBool(key)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to parse server OS type:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to parse server OS type: "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,8 +61,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
// read private key
|
||||
key, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to read private key file:", keyFile+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to read private key: "+keyFile, core.ErrorRead, true)
|
||||
}
|
||||
|
||||
// parse private key
|
||||
@@ -71,19 +69,17 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
if keyFileProtected != "true" {
|
||||
parsedKey, err = ssh.ParsePrivateKey(key)
|
||||
} else {
|
||||
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, inputKeyFilePassphrase())
|
||||
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, core.PassphraseInputFunction("Enter passphrase for your SSH keyfile:"))
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to parse private key:", keyFile+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to parse private key: "+keyFile, core.ErrorRead, true)
|
||||
}
|
||||
|
||||
// read known hosts file
|
||||
var hostKeyCallback ssh.HostKeyCallback
|
||||
hostKeyCallback, err = knownhosts.New(core.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts")
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to read known hosts file:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to read known hosts file: "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
|
||||
// configure SSH client
|
||||
@@ -93,13 +89,14 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
||||
ssh.PublicKeys(parsedKey),
|
||||
},
|
||||
HostKeyCallback: hostKeyCallback,
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
// connect to SSH server
|
||||
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to connect to remote server:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
core.PrintError("Sync failed - Unable to connect to remote server: "+err.Error(), core.ErrorServerConnection, false) // do not crash/close interactive clients
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
return sshClient, entryRoot, isWindows
|
||||
@@ -110,8 +107,7 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string {
|
||||
// create a session
|
||||
sshSession, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to establish SSH session:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
core.PrintError("Sync failed - Unable to establish SSH session: "+err.Error(), core.ErrorServerConnection, true)
|
||||
}
|
||||
|
||||
// provide stdin data for session
|
||||
@@ -121,8 +117,7 @@ func GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) string {
|
||||
var output []byte
|
||||
output, err = sshSession.CombinedOutput(cmd)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to run SSH command:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorSyncProcess)
|
||||
core.PrintError("Sync failed - Unable to run SSH command: "+err.Error(), core.ErrorSyncProcess, true)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if len(*deviceIDList) == 0 {
|
||||
if manualSync {
|
||||
fmt.Println(joinErrorWithEXE("Sync failed - No device ID found; run \"", " init\" to generate a device ID"))
|
||||
os.Exit(core.ErrorTargetNotFound)
|
||||
core.PrintError("Sync failed - No device ID found", core.ErrorTargetNotFound, true)
|
||||
} else {
|
||||
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
|
||||
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)
|
||||
os.Exit(core.ErrorSyncProcess)
|
||||
core.PrintError("Sync failed - Unable to fetch remote data; server returned an unexpected response", core.ErrorSyncProcess, true)
|
||||
}
|
||||
serverTime, err := strconv.ParseInt(outputSlice[0], 10, 64)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to parse server time:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to parse server time: "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
entries := strings.Split(outputSlice[1], 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 {
|
||||
mod, err = strconv.ParseInt(modString, 10, 64)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to parse mod time:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to parse mod time: "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
mods = append(mods, mod)
|
||||
}
|
||||
@@ -218,14 +209,12 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
// create an SFTP client from sshClient
|
||||
sftpClient, err := sftp.NewClient(sshClient)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to establish SFTP session:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
core.PrintError("Sync failed - Unable to establish SFTP session: "+err.Error(), core.ErrorServerConnection, true)
|
||||
}
|
||||
defer func(sftpClient *sftp.Client) {
|
||||
err = sftpClient.Close()
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SFTP client:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
core.PrintError("Sync failed - Unable to close SFTP client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
}
|
||||
}(sftpClient)
|
||||
|
||||
@@ -243,8 +232,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
var fileInfo os.FileInfo
|
||||
fileInfo, err = sftpClient.Stat(remoteEntryFullPath)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to get remote file info (mod time):", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to get remote file info (mod time): "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
modTime := fileInfo.ModTime()
|
||||
|
||||
@@ -252,8 +240,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
var remoteFile *sftp.File
|
||||
remoteFile, err = sftpClient.Open(remoteEntryFullPath)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to open remote file:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to open remote file: "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
|
||||
// store path to local entry
|
||||
@@ -263,15 +250,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
var localFile *os.File
|
||||
localFile, err = os.OpenFile(localEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to create local file:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorWrite)
|
||||
core.PrintError("Sync failed - Unable to create local file: "+err.Error(), core.ErrorWrite, true)
|
||||
}
|
||||
|
||||
// download the file
|
||||
_, err = remoteFile.WriteTo(localFile)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to download remote file:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorSyncProcess)
|
||||
core.PrintError("Sync failed - Unable to download remote file: "+err.Error(), core.ErrorSyncProcess, true)
|
||||
}
|
||||
|
||||
// close the files
|
||||
@@ -300,8 +285,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
var fileInfo os.FileInfo
|
||||
fileInfo, err = os.Stat(localEntryFullPath)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to get local file info (mod time):", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to get local file info (mod time): "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
modTime := fileInfo.ModTime()
|
||||
|
||||
@@ -309,8 +293,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
var localFile *os.File
|
||||
localFile, err = os.Open(localEntryFullPath)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to open local file:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Sync failed - Unable to open local file: "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
|
||||
// store path to remote entry
|
||||
@@ -320,15 +303,13 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
var remoteFile *sftp.File
|
||||
remoteFile, err = sftpClient.OpenFile(remoteEntryFullPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to create remote file ("+remoteEntryFullPath+"):", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorWrite)
|
||||
core.PrintError("Sync failed - Unable to create remote file ("+remoteEntryFullPath+"): "+err.Error(), core.ErrorWrite, true)
|
||||
}
|
||||
|
||||
// upload the file
|
||||
_, err = localFile.WriteTo(remoteFile)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to upload local file:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorSyncProcess)
|
||||
core.PrintError("Sync failed - Unable to upload local file: "+err.Error(), core.ErrorSyncProcess, true)
|
||||
}
|
||||
|
||||
// close the files
|
||||
@@ -338,8 +319,7 @@ func sftpSync(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows bool, dow
|
||||
// set permissions on remote file
|
||||
err = sftpClient.Chmod(remoteEntryFullPath, 0600)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to set permissions on remote file:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorSyncProcess)
|
||||
core.PrintError("Sync failed - Unable to set permissions on remote file: "+err.Error(), core.ErrorSyncProcess, true)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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
|
||||
var downloadList, uploadList []string
|
||||
|
||||
@@ -393,6 +373,11 @@ func syncLists(sshClient *ssh.Client, sshEntryRoot string, sshIsWindows, timeSyn
|
||||
}
|
||||
|
||||
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).
|
||||
@@ -403,8 +388,7 @@ func deletionSync(deletions []string) {
|
||||
fmt.Println(ansiDelete+deletion+core.AnsiReset, "has been sheared, removing locally (if it exists)")
|
||||
err := os.RemoveAll(core.TargetLocationFormat(deletion))
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Failed to shear "+deletion+" locally:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorWrite)
|
||||
core.PrintError("Sync failed - Failed to shear "+deletion+" locally: "+err.Error(), core.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,38 +409,39 @@ func folderSync(folders []string) {
|
||||
if !isFile && !isAccessible {
|
||||
err := os.MkdirAll(folderFullPath, 0700)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Failed to create folder \""+folder+"\":", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorWrite)
|
||||
core.PrintError("Sync failed - Failed to create folder ("+folder+"): "+err.Error(), core.ErrorWrite, true)
|
||||
}
|
||||
} else if isFile {
|
||||
fmt.Println(core.AnsiError + "Sync failed - Failed to create folder \"" + folder + "\" - A file with the same name already exists" + core.AnsiReset)
|
||||
os.Exit(core.ErrorTargetExists)
|
||||
core.PrintError("Sync failed - Failed to create folder ("+folder+") - A file with the same name already exists", core.ErrorTargetExists, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunJob runs the SSH sync job.
|
||||
// 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
|
||||
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) {
|
||||
err := sshClient.Close()
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
core.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
}
|
||||
}(sshClient)
|
||||
|
||||
// fetch remote lists
|
||||
remoteEntryModMap, remoteFolders, deletions, serverTime, clientTime := getRemoteDataFromClient(sshClient, manualSync)
|
||||
|
||||
// sync folders
|
||||
folderSync(remoteFolders)
|
||||
|
||||
// sync deletions
|
||||
deletionSync(deletions)
|
||||
|
||||
// sync folders
|
||||
folderSync(remoteFolders)
|
||||
|
||||
// fetch local lists
|
||||
localEntryModMap := getLocalData()
|
||||
|
||||
@@ -469,8 +454,13 @@ func RunJob(manualSync bool) {
|
||||
}
|
||||
|
||||
// sync new and updated entries
|
||||
syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, localEntryModMap, remoteEntryModMap)
|
||||
|
||||
// exit program after successful sync
|
||||
core.Exit(0)
|
||||
var lists [3][]string
|
||||
if returnLists {
|
||||
lists = syncLists(sshClient, sshEntryRoot, sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap)
|
||||
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
-14
@@ -20,10 +20,11 @@ func getModTimes(entryList []string) []int64 {
|
||||
}
|
||||
|
||||
// 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).
|
||||
// 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
|
||||
var onServer bool
|
||||
if clientDeviceID != "" {
|
||||
@@ -49,19 +50,19 @@ func ShearLocal(targetLocationIncomplete, clientDeviceID string) string {
|
||||
|
||||
// get the full targetLocation path and remove the target
|
||||
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
|
||||
core.TargetIsFile(targetLocationComplete, true, 0)
|
||||
isFile, _ = core.TargetIsFile(targetLocationComplete, true, 0)
|
||||
}
|
||||
err := os.RemoveAll(targetLocationComplete)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Failed to remove local target:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorWrite)
|
||||
core.PrintError("Failed to remove local target: "+err.Error(), core.ErrorWrite, true)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -80,14 +81,13 @@ func RenameLocal(oldLocationIncomplete, newLocationIncomplete string, verifyOldL
|
||||
// ensure newLocation does not exist
|
||||
_, isAccessible := core.TargetIsFile(newLocation, false, 0)
|
||||
if isAccessible {
|
||||
fmt.Println(core.AnsiError + "\"" + newLocation + "\" already exists" + core.AnsiReset)
|
||||
os.Exit(core.ErrorTargetExists)
|
||||
core.PrintError("\""+newLocation+"\" already exists", core.ErrorTargetExists, true)
|
||||
}
|
||||
|
||||
// rename oldLocation to newLocation
|
||||
err := os.Rename(oldLocation, newLocation)
|
||||
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)
|
||||
}
|
||||
|
||||
// do not exit program, as this function is used as part of RenameRemoteFromClient
|
||||
@@ -101,11 +101,9 @@ func AddFolderLocal(targetLocationIncomplete string) {
|
||||
err := os.Mkdir(targetLocationComplete, 0700)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
fmt.Println(core.AnsiError + "Directory already exists" + core.AnsiReset)
|
||||
os.Exit(core.ErrorTargetExists)
|
||||
fmt.Println(ansiUpload + "Directory already exists - libmutton will still ensure it exists on the server")
|
||||
} else {
|
||||
fmt.Println(core.AnsiError+"Failed to create directory:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorWrite)
|
||||
core.PrintError("Failed to create directory: "+err.Error(), core.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-11
@@ -3,7 +3,6 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -25,16 +24,14 @@ func WalkEntryDir() ([]string, []string) {
|
||||
// check for errors encountered while walking directory
|
||||
if err != nil {
|
||||
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 {
|
||||
// otherwise, print the source of the error
|
||||
fmt.Println(core.AnsiError+"An unexpected error occurred while generating the entry list:", err.Error()+core.AnsiReset)
|
||||
core.PrintError("An unexpected error occurred while generating the entry list: "+err.Error(), core.ErrorOther, true)
|
||||
}
|
||||
os.Exit(core.ErrorOther)
|
||||
}
|
||||
|
||||
// trim root path from each path before storing
|
||||
trimmedPath := fullPath[rootLength:]
|
||||
trimmedPath := fullPath[RootLength:]
|
||||
|
||||
// append the path to the appropriate slice
|
||||
if !entry.IsDir() {
|
||||
@@ -48,8 +45,3 @@ func WalkEntryDir() ([]string, []string) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+3
-11
@@ -3,7 +3,6 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -26,16 +25,14 @@ func WalkEntryDir() ([]string, []string) {
|
||||
// check for errors encountered while walking directory
|
||||
if err != nil {
|
||||
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 {
|
||||
// otherwise, print the source of the error
|
||||
fmt.Println(core.AnsiError+"An unexpected error occurred while generating the entry list:", err.Error()+core.AnsiReset)
|
||||
core.PrintError("An unexpected error occurred while generating the entry list: "+err.Error(), core.ErrorOther, true)
|
||||
}
|
||||
os.Exit(core.ErrorOther)
|
||||
}
|
||||
|
||||
// trim root path from each path before storing and replace backslashes with forward slashes
|
||||
trimmedPath := strings.ReplaceAll(fullPath[rootLength:], "\\", "/")
|
||||
trimmedPath := strings.ReplaceAll(fullPath[RootLength:], "\\", "/")
|
||||
|
||||
// append the path to the appropriate slice
|
||||
if !entry.IsDir() {
|
||||
@@ -49,8 +46,3 @@ func WalkEntryDir() ([]string, []string) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+9
-13
@@ -1,7 +1,6 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -18,17 +17,22 @@ import (
|
||||
func DeviceIDGen(oldDeviceID string) (string, string) {
|
||||
// generate new device ID
|
||||
deviceIDPrefix, _ := os.Hostname()
|
||||
deviceIDSuffix := core.StringGen(rand.Intn(32)+48, true, 0.2, true) + "-" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
deviceIDSuffix := core.StringGen(rand.Intn(32)+48, 0.2, 1) + "-" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
newDeviceID := deviceIDPrefix + "-" + deviceIDSuffix
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Failed to create local device ID file:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorWrite)
|
||||
core.PrintError("Failed to create local device ID file: "+err.Error(), core.ErrorWrite, true)
|
||||
}
|
||||
_ = fileToClose.Close() // error ignored; if the file could be created, it can probably be closed
|
||||
|
||||
// remove old device ID file (locally; may not exist)
|
||||
err = os.RemoveAll(core.ConfigDir + core.PathSeparator + "devices" + core.PathSeparator + oldDeviceID)
|
||||
if err != nil {
|
||||
core.PrintError("Failed to remove old device ID file (locally): "+err.Error(), core.ErrorWrite, true)
|
||||
}
|
||||
|
||||
// register new device ID with server and fetch remote EntryRoot and OS type
|
||||
// also removes the old device ID file (remotely)
|
||||
// manualSync is true so the user is alerted if device ID registration fails
|
||||
@@ -36,15 +40,7 @@ func DeviceIDGen(oldDeviceID string) (string, string) {
|
||||
sshEntryRootSSHIsWindows := strings.Split(GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), core.FSSpace)
|
||||
err = sshClient.Close()
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Init failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
}
|
||||
|
||||
// remove old device ID file (locally; may not exist)
|
||||
err = os.RemoveAll(core.ConfigDir + core.PathSeparator + "devices" + core.PathSeparator + oldDeviceID)
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Failed to remove old device ID file (locally):", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorWrite)
|
||||
core.PrintError("Init failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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.
|
||||
// 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) {
|
||||
deviceID := ShearLocal(targetLocationIncomplete, "") // remove the target from the local system and get the device ID of the client
|
||||
func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) {
|
||||
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
|
||||
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
|
||||
GetSSHOutput(sshClient, "libmuttonserver shear", deviceID+"\n"+strings.ReplaceAll(targetLocationIncomplete, core.PathSeparator, core.FSPath))
|
||||
|
||||
// close the SSH client
|
||||
err := sshClient.Close()
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
core.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// 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
|
||||
|
||||
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
|
||||
sshClient, _, _ := GetSSHClient(false)
|
||||
|
||||
@@ -50,8 +52,7 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string)
|
||||
// close the SSH client
|
||||
err := sshClient.Close()
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
core.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// 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
|
||||
|
||||
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
|
||||
sshClient, _, _ := GetSSHClient(false)
|
||||
|
||||
@@ -74,8 +75,7 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string) {
|
||||
// close the SSH client
|
||||
err := sshClient.Close()
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Sync failed - Unable to close SSH client:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorServerConnection)
|
||||
core.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), core.ErrorServerConnection, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -17,8 +17,7 @@ func GetRemoteDataFromServer(clientDeviceID string) {
|
||||
modList := getModTimes(entryList)
|
||||
deletionsList, err := os.ReadDir(core.ConfigDir + core.PathSeparator + "deletions")
|
||||
if err != nil {
|
||||
fmt.Println(core.AnsiError+"Failed to read the deletions directory:", err.Error()+core.AnsiReset)
|
||||
os.Exit(core.ErrorRead)
|
||||
core.PrintError("Failed to read the deletions directory: "+err.Error(), core.ErrorRead, true)
|
||||
}
|
||||
|
||||
// print the current UNIX timestamp to stdout
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
## Known Bugs - libmutton
|
||||
- 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
|
||||
- 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.
|
||||
|
||||
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)
|
||||
- `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
|
||||
libmutton-based password manager clients should accept at least one specific required argument, as well as another recommended one:
|
||||
- `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 Global Variable Manipulation
|
||||
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.
|
||||
|
||||
## 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
|
||||
libmutton-based password manager clients should all share the same INI configuration file.
|
||||
|
||||
Reference in New Issue
Block a user