// 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" "runtime" "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 } func PrintMemoryUsage() { for { var m runtime.MemStats runtime.ReadMemStats(&m) LOG_INFO("MEMORY_STATS", "Currently allocated: %s, total allocated: %s, system memory: %s, number of GCs: %d", PrettySize(int64(m.Alloc)), PrettySize(int64(m.TotalAlloc)), PrettySize(int64(m.Sys)), m.NumGC) time.Sleep(time.Second) } }