bisync: don't convert modtime precision in listings - fixes #8025

Before this change, bisync proactively converted modtime precision when greater
than what the destination backend supported.

This dates back to a time before bisync considered the modifyWindow for same-side
comparisons. Back then, it was problematic to save a listing with 12:54:49.7 for
a backend that can't handle that precision, as on the next run the backend would
report the time as 12:54:50 and bisync would think the file had changed. So the
truncation was a workaround to anticipate this and proactively record the time
with the precision we expect to receive next time.

However, this caused problems for backends (such as dropbox) that round instead
of truncating as bisync expected.

After this change, bisync preserves the original precision in the listing
(without conversion), even when greater than what the backend supports, to avoid
rounding error. On the next run, bisync will compare it to the rounded time
reported by the backend, and if it's within the modifyWindow, it will treat them
as equivalent.
This commit is contained in:
nielash 2024-08-23 11:42:36 -04:00
parent 146562975b
commit 956c2963fd
2 changed files with 16 additions and 26 deletions

View File

@ -43,8 +43,10 @@ var lineRegex = regexp.MustCompile(`^(\S) +(-?\d+) (\S+) (\S+) (\d{4}-\d\d-\d\dT
const timeFormat = "2006-01-02T15:04:05.000000000-0700" const timeFormat = "2006-01-02T15:04:05.000000000-0700"
// TZ defines time zone used in listings // TZ defines time zone used in listings
var TZ = time.UTC var (
var tzLocal = false TZ = time.UTC
tzLocal = false
)
// fileInfo describes a file // fileInfo describes a file
type fileInfo struct { type fileInfo struct {
@ -83,7 +85,7 @@ func (ls *fileList) has(file string) bool {
} }
_, found := ls.info[file] _, found := ls.info[file]
if !found { if !found {
//try unquoting // try unquoting
file, _ = strconv.Unquote(`"` + file + `"`) file, _ = strconv.Unquote(`"` + file + `"`)
_, found = ls.info[file] _, found = ls.info[file]
} }
@ -93,7 +95,7 @@ func (ls *fileList) has(file string) bool {
func (ls *fileList) get(file string) *fileInfo { func (ls *fileList) get(file string) *fileInfo {
info, found := ls.info[file] info, found := ls.info[file]
if !found { if !found {
//try unquoting // try unquoting
file, _ = strconv.Unquote(`"` + file + `"`) file, _ = strconv.Unquote(`"` + file + `"`)
info = ls.info[fmt.Sprint(file)] info = ls.info[fmt.Sprint(file)]
} }
@ -420,7 +422,7 @@ func (b *bisyncRun) loadListingNum(listingNum int) (*fileList, error) {
func (b *bisyncRun) listDirsOnly(listingNum int) (*fileList, error) { func (b *bisyncRun) listDirsOnly(listingNum int) (*fileList, error) {
var fulllisting *fileList var fulllisting *fileList
var dirsonly = newFileList() dirsonly := newFileList()
var err error var err error
if !b.opt.CreateEmptySrcDirs { if !b.opt.CreateEmptySrcDirs {
@ -450,24 +452,6 @@ func (b *bisyncRun) listDirsOnly(listingNum int) (*fileList, error) {
return dirsonly, err return dirsonly, err
} }
// ConvertPrecision returns the Modtime rounded to Dest's precision if lower, otherwise unchanged
// Need to use the other fs's precision (if lower) when copying
// Note: we need to use Truncate rather than Round so that After() is reliable.
// (2023-11-02 20:22:45.552679442 +0000 < UTC 2023-11-02 20:22:45.553 +0000 UTC)
func ConvertPrecision(Modtime time.Time, dst fs.Fs) time.Time {
DestPrecision := dst.Precision()
// In case it's wrapping an Fs with lower precision, try unwrapping and use the lowest.
if Modtime.Truncate(DestPrecision).After(Modtime.Truncate(fs.UnWrapFs(dst).Precision())) {
DestPrecision = fs.UnWrapFs(dst).Precision()
}
if Modtime.After(Modtime.Truncate(DestPrecision)) {
return Modtime.Truncate(DestPrecision)
}
return Modtime
}
// modifyListing will modify the listing based on the results of the sync // modifyListing will modify the listing based on the results of the sync
func (b *bisyncRun) modifyListing(ctx context.Context, src fs.Fs, dst fs.Fs, results []Results, queues queues, is1to2 bool) (err error) { func (b *bisyncRun) modifyListing(ctx context.Context, src fs.Fs, dst fs.Fs, results []Results, queues queues, is1to2 bool) (err error) {
queue := queues.copy2to1 queue := queues.copy2to1
@ -533,13 +517,13 @@ func (b *bisyncRun) modifyListing(ctx context.Context, src fs.Fs, dst fs.Fs, res
// build src winners list // build src winners list
if result.IsSrc && result.Src != "" && (result.Winner.Err == nil || result.Flags == "d") { if result.IsSrc && result.Src != "" && (result.Winner.Err == nil || result.Flags == "d") {
srcWinners.put(result.Name, result.Size, ConvertPrecision(result.Modtime, src), result.Hash, "-", result.Flags) srcWinners.put(result.Name, result.Size, result.Modtime, result.Hash, "-", result.Flags)
prettyprint(result, "winner: copy to src", fs.LogLevelDebug) prettyprint(result, "winner: copy to src", fs.LogLevelDebug)
} }
// build dst winners list // build dst winners list
if result.IsWinner && result.Winner.Side != "none" && (result.Winner.Err == nil || result.Flags == "d") { if result.IsWinner && result.Winner.Side != "none" && (result.Winner.Err == nil || result.Flags == "d") {
dstWinners.put(result.Name, result.Size, ConvertPrecision(result.Modtime, dst), result.Hash, "-", result.Flags) dstWinners.put(result.Name, result.Size, result.Modtime, result.Hash, "-", result.Flags)
prettyprint(result, "winner: copy to dst", fs.LogLevelDebug) prettyprint(result, "winner: copy to dst", fs.LogLevelDebug)
} }
@ -623,7 +607,7 @@ func (b *bisyncRun) modifyListing(ctx context.Context, src fs.Fs, dst fs.Fs, res
} }
if srcNewName != "" { // if it was renamed and not deleted if srcNewName != "" { // if it was renamed and not deleted
srcList.put(srcNewName, new.size, new.time, new.hash, new.id, new.flags) srcList.put(srcNewName, new.size, new.time, new.hash, new.id, new.flags)
dstList.put(srcNewName, new.size, ConvertPrecision(new.time, src), new.hash, new.id, new.flags) dstList.put(srcNewName, new.size, new.time, new.hash, new.id, new.flags)
} }
if srcNewName != srcOldName { if srcNewName != srcOldName {
srcList.remove(srcOldName) srcList.remove(srcOldName)

View File

@ -1812,6 +1812,12 @@ about _Unison_ and synchronization in general.
## Changelog ## Changelog
### `v1.68`
* Fixed an issue affecting backends that round modtimes to a lower precision.
### `v1.67`
* Added integration tests against all backends.
### `v1.66` ### `v1.66`
* Copies and deletes are now handled in one operation instead of two * Copies and deletes are now handled in one operation instead of two
* `--track-renames` and `--backup-dir` are now supported * `--track-renames` and `--backup-dir` are now supported