1
0
mirror of https://github.com/gilbertchen/duplicacy synced 2025-12-06 00:03:38 +00:00
Files
duplicacy/src/duplicacy_utils.go
2019-04-26 23:47:25 -04:00

463 lines
11 KiB
Go

// Copyright (c) Acrosync LLC. All rights reserved.
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
import (
"bufio"
"crypto/sha256"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/gilbertchen/gopass"
"golang.org/x/crypto/pbkdf2"
)
var RunInBackground bool = false
type RateLimitedReader struct {
Content []byte
Rate float64
Next int
StartTime time.Time
}
var RegexMap map[string]*regexp.Regexp
func init() {
if RegexMap == nil {
RegexMap = make(map[string]*regexp.Regexp)
}
}
func CreateRateLimitedReader(content []byte, rate int) *RateLimitedReader {
return &RateLimitedReader{
Content: content,
Rate: float64(rate * 1024),
Next: 0,
}
}
func IsEmptyFilter(pattern string) bool {
if pattern == "+" || pattern == "-" || pattern == "i:" || pattern == "e:" {
return true
} else {
return false
}
}
func IsUnspecifiedFilter(pattern string) bool {
if pattern[0] != '+' && pattern[0] != '-' && !strings.HasPrefix(pattern, "i:") && !strings.HasPrefix(pattern, "e:") {
return true
} else {
return false
}
}
func IsValidRegex(pattern string) (valid bool, err error) {
var re *regexp.Regexp = nil
if re, valid = RegexMap[pattern]; valid && re != nil {
return true, nil
}
re, err = regexp.Compile(pattern)
if err != nil {
return false, err
} else {
RegexMap[pattern] = re
LOG_DEBUG("REGEX_STORED", "Saved compiled regex for pattern \"%s\", regex=%#v", pattern, re)
return true, err
}
}
func (reader *RateLimitedReader) Length() int64 {
return int64(len(reader.Content))
}
func (reader *RateLimitedReader) Reset() {
reader.Next = 0
}
func (reader *RateLimitedReader) Seek(offset int64, whence int) (int64, error) {
if whence == io.SeekStart {
reader.Next = int(offset)
} else if whence == io.SeekCurrent {
reader.Next += int(offset)
} else {
reader.Next = len(reader.Content) - int(offset)
}
return int64(reader.Next), nil
}
func (reader *RateLimitedReader) Read(p []byte) (n int, err error) {
if reader.Next >= len(reader.Content) {
return 0, io.EOF
}
if reader.Rate <= 0 {
n := copy(p, reader.Content[reader.Next:])
reader.Next += n
if reader.Next >= len(reader.Content) {
return n, io.EOF
}
return n, nil
}
if reader.StartTime.IsZero() {
reader.StartTime = time.Now()
}
elapsed := time.Since(reader.StartTime).Seconds()
delay := float64(reader.Next)/reader.Rate - elapsed
end := reader.Next + int(reader.Rate/5)
if delay > 0 {
time.Sleep(time.Duration(delay * float64(time.Second)))
} else {
end += -int(delay * reader.Rate)
}
if end > len(reader.Content) {
end = len(reader.Content)
}
n = copy(p, reader.Content[reader.Next:end])
reader.Next += n
return n, nil
}
func RateLimitedCopy(writer io.Writer, reader io.Reader, rate int) (written int64, err error) {
if rate <= 0 {
return io.Copy(writer, reader)
}
for range time.Tick(time.Second / 5) {
n, err := io.CopyN(writer, reader, int64(rate*1024/5))
written += n
if err != nil {
if err == io.EOF {
return written, nil
} else {
return written, err
}
}
}
return written, nil
}
// GenerateKeyFromPassword generates a key from the password.
func GenerateKeyFromPassword(password string, salt []byte, iterations int) []byte {
return pbkdf2.Key([]byte(password), salt, iterations, 32, sha256.New)
}
// Get password from preference, env, but don't start any keyring request
func GetPasswordFromPreference(preference Preference, passwordType string) string {
passwordID := passwordType
if preference.Name != "default" {
passwordID = preference.Name + "_" + passwordID
}
{
name := strings.ToUpper("duplicacy_" + passwordID)
LOG_DEBUG("PASSWORD_ENV_VAR", "Reading the environment variable %s", name)
if password, found := os.LookupEnv(name); found && password != "" {
return password
}
re := regexp.MustCompile(`[^a-zA-Z0-9_]`)
namePlain := re.ReplaceAllString(name, "_")
if namePlain != name {
LOG_DEBUG("PASSWORD_ENV_VAR", "Reading the environment variable %s", namePlain)
if password, found := os.LookupEnv(namePlain); found && password != "" {
return password
}
}
}
// If the password is stored in the preference, there is no need to include the storage name
// (i.e., preference.Name) in the key, so the key name should really be passwordType rather
// than passwordID; we're using passwordID here only for backward compatibility
if len(preference.Keys) > 0 && len(preference.Keys[passwordID]) > 0 {
LOG_DEBUG("PASSWORD_PREFERENCE", "Reading %s from preferences", passwordID)
return preference.Keys[passwordID]
}
if len(preference.Keys) > 0 && len(preference.Keys[passwordType]) > 0 {
LOG_DEBUG("PASSWORD_PREFERENCE", "Reading %s from preferences", passwordType)
return preference.Keys[passwordType]
}
return ""
}
// GetPassword attempts to get the password from KeyChain/KeyRing, environment variables, or keyboard input.
func GetPassword(preference Preference, passwordType string, prompt string,
showPassword bool, resetPassword bool) string {
passwordID := passwordType
preferencePassword := GetPasswordFromPreference(preference, passwordType)
if preferencePassword != "" {
return preferencePassword
}
if preference.Name != "default" {
passwordID = preference.Name + "_" + passwordID
}
if resetPassword && !RunInBackground {
keyringSet(passwordID, "")
} else {
password := keyringGet(passwordID)
if password != "" {
LOG_DEBUG("PASSWORD_KEYCHAIN", "Reading %s from keychain/keyring", passwordType)
return password
}
if RunInBackground {
LOG_INFO("PASSWORD_MISSING", "%s is not found in Keychain/Keyring", passwordID)
return ""
}
}
password := ""
fmt.Printf("%s", prompt)
if showPassword {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
password = scanner.Text()
} else {
passwordInBytes, err := gopass.GetPasswdMasked()
if err != nil {
LOG_ERROR("PASSWORD_READ", "Failed to read the password: %v", err)
return ""
}
password = string(passwordInBytes)
}
return password
}
// SavePassword saves the specified password in the keyring/keychain.
func SavePassword(preference Preference, passwordType string, password string) {
if password == "" || RunInBackground {
return
}
if preference.DoNotSavePassword {
return
}
// If the password is retrieved from env or preference, don't save it to keyring
if GetPasswordFromPreference(preference, passwordType) == password {
return
}
passwordID := passwordType
if preference.Name != "default" {
passwordID = preference.Name + "_" + passwordID
}
keyringSet(passwordID, password)
}
// The following code was modified from the online article 'Matching Wildcards: An Algorithm', by Kirk J. Krauss,
// Dr. Dobb's, August 26, 2008. However, the version in the article doesn't handle cases like matching 'abcccd'
// against '*ccd', and the version here fixed that issue.
//
func matchPattern(text string, pattern string) bool {
textLength := len(text)
patternLength := len(pattern)
afterLastWildcard := 0
afterLastMatched := 0
t := 0
p := 0
for {
if t >= textLength {
if p >= patternLength {
return true // "x" matches "x"
} else if pattern[p] == '*' {
p++
continue // "x*" matches "x" or "xy"
}
return false // "x" doesn't match "xy"
}
w := byte(0)
if p < patternLength {
w = pattern[p]
}
if text[t] != w {
if w == '?' {
t++
p++
continue
} else if w == '*' {
p++
afterLastWildcard = p
if p >= patternLength {
return true
}
} else if afterLastWildcard > 0 {
p = afterLastWildcard
t = afterLastMatched
t++
} else {
return false
}
for t < textLength && text[t] != pattern[p] && pattern[p] != '?' {
t++
}
if t >= textLength {
return false
}
afterLastMatched = t
}
t++
p++
}
}
// MatchPath returns 'true' if the file 'filePath' is excluded by the specified 'patterns'. Each pattern starts with
// either '+' or '-', whereas '-' indicates exclusion and '+' indicates inclusion. Wildcards like '*' and '?' may
// appear in the patterns. In case no matching pattern is found, the file will be excluded if all patterns are
// include patterns, and included otherwise.
func MatchPath(filePath string, patterns []string) (included bool) {
var re *regexp.Regexp = nil
var found bool
var matched bool
allIncludes := true
for _, pattern := range patterns {
if pattern[0] == '+' {
if matchPattern(filePath, pattern[1:]) {
LOG_DEBUG("PATTERN_INCLUDE", "%s is included by pattern %s", filePath, pattern)
return true
}
} else if pattern[0] == '-' {
allIncludes = false
if matchPattern(filePath, pattern[1:]) {
LOG_DEBUG("PATTERN_EXCLUDE", "%s is excluded by pattern %s", filePath, pattern)
return false
}
} else if strings.HasPrefix(pattern, "i:") || strings.HasPrefix(pattern, "e:") {
if re, found = RegexMap[pattern[2:]]; found {
matched = re.MatchString(filePath)
} else {
re, err := regexp.Compile(pattern)
if err != nil {
LOG_ERROR("REGEX_ERROR", "Invalid regex encountered for pattern \"%s\" - %v", pattern[2:], err)
}
RegexMap[pattern] = re
matched = re.MatchString(filePath)
}
if matched {
if strings.HasPrefix(pattern, "i:") {
LOG_DEBUG("PATTERN_INCLUDE", "%s is included by pattern %s", filePath, pattern)
return true
} else {
LOG_DEBUG("PATTERN_EXCLUDE", "%s is excluded by pattern %s", filePath, pattern)
return false
}
} else {
if strings.HasPrefix(pattern, "e:") {
allIncludes = false
}
}
}
}
if allIncludes {
LOG_DEBUG("PATTERN_EXCLUDE", "%s is excluded", filePath)
return false
} else {
LOG_DEBUG("PATTERN_INCLUDE", "%s is included", filePath)
return true
}
}
func PrettyNumber(number int64) string {
G := int64(1024 * 1024 * 1024)
M := int64(1024 * 1024)
K := int64(1024)
if number > 1000*G {
return fmt.Sprintf("%dG", number/G)
} else if number > G {
return fmt.Sprintf("%d,%03dM", number/(1000*M), (number/M)%1000)
} else if number > M {
return fmt.Sprintf("%d,%03dK", number/(1000*K), (number/K)%1000)
} else if number > K {
return fmt.Sprintf("%dK", number/K)
} else {
return fmt.Sprintf("%d", number)
}
}
func PrettySize(size int64) string {
if size > 1024*1024 {
return fmt.Sprintf("%.2fM", float64(size)/(1024.0*1024.0))
} else if size > 1024 {
return fmt.Sprintf("%.0fK", float64(size)/1024.0)
} else {
return fmt.Sprintf("%d", size)
}
}
func PrettyTime(seconds int64) string {
day := int64(3600 * 24)
if seconds > day*2 {
return fmt.Sprintf("%d days %02d:%02d:%02d",
seconds/day, (seconds%day)/3600, (seconds%3600)/60, seconds%60)
} else if seconds > day {
return fmt.Sprintf("1 day %02d:%02d:%02d", (seconds%day)/3600, (seconds%3600)/60, seconds%60)
} else if seconds > 0 {
return fmt.Sprintf("%02d:%02d:%02d", seconds/3600, (seconds%3600)/60, seconds%60)
} else {
return "n/a"
}
}
func AtoSize(sizeString string) int {
sizeString = strings.ToLower(sizeString)
sizeRegex := regexp.MustCompile(`^([0-9]+)([mk])?$`)
matched := sizeRegex.FindStringSubmatch(sizeString)
if matched == nil {
return 0
}
size, _ := strconv.Atoi(matched[1])
if matched[2] == "m" {
size *= 1024 * 1024
} else if matched[2] == "k" {
size *= 1024
}
return size
}