]> arthur.barton.de Git - bup.git/commitdiff
Add a helpers.FSTime class to handle filesystem timestamps and use it.
authorRob Browning <rlb@defaultvalue.org>
Sun, 24 Oct 2010 20:30:46 +0000 (15:30 -0500)
committerRob Browning <rlb@defaultvalue.org>
Sun, 24 Oct 2010 20:30:46 +0000 (15:30 -0500)
Add helpers.FSTime class to handle filesystem timestamps, and avoid
scattered conditionals.  At the moment, when _helpers.lstat is
available, FSTime stores timestamps as integer nanoseconds, otherwise
it uses the Python native stat representation (i.e. floating-point
seconds).  Operations like from_stat_time, from_timespec, to_timespec,
and approx_secs handle the appropriate conversions.

Add FSTime tests, and test both representations when the platform has
_helpers.lstat.

lib/bup/helpers.py
lib/bup/metadata.py
lib/bup/t/thelpers.py
lib/bup/t/tmetadata.py

index f7688da7fbc733a1e1c4676031968ba704f917bc..1223456b0f889c40516c99d5ec1e974a3994d9c6 100644 (file)
@@ -413,10 +413,84 @@ def parse_date_or_fatal(str, fatal):
         return date
 
 
+class FSTime():
+    # Class to represent filesystem timestamps.  Use integer
+    # nanoseconds on platforms where we have the higher resolution
+    # lstat.  Use the native python stat representation (floating
+    # point seconds) otherwise.
+
+    def __cmp__(self, x):
+        return self._value.__cmp__(x._value)
+
+    def to_timespec(self):
+        """Return (s, ns) where ns is always non-negative
+        and t = s + ns / 10e8""" # metadata record rep (and libc rep)
+        s_ns = self.secs_nsecs()
+        if s_ns[0] > 0 or s_ns[1] >= 0:
+            return s_ns
+        return (s_ns[0] - 1, 10**9 + s_ns[1]) # ns is negative
+
+    if _helpers.lstat: # Use integer nanoseconds.
+
+        @staticmethod
+        def from_secs(secs):
+            ts = FSTime()
+            ts._value = int(secs * 10**9)
+            return ts
+
+        @staticmethod
+        def from_timespec(timespec):
+            ts = FSTime()
+            ts._value = timespec[0] * 10**9 + timespec[1]
+            return ts
+
+        @staticmethod
+        def from_stat_time(stat_time):
+            return FSTime.from_timespec(stat_time)
+
+        def approx_secs(self):
+            return self._value / 10e8;
+
+        def secs_nsecs(self):
+            "Return a (s, ns) pair: -1.5s -> (-1, -10**9 / 2)."
+            if self._value >= 0:
+                return (self._value / 10**9, self._value % 10**9)
+            abs_val = -self._value
+            return (- (abs_val / 10**9), - (abs_val % 10**9))
+
+    else: # Use python default floating-point seconds.
+
+        @staticmethod
+        def from_secs(secs):
+            ts = FSTime()
+            ts._value = secs
+            return ts
+
+        @staticmethod
+        def from_timespec(timespec):
+            ts = FSTime()
+            ts._value = timespec[0] + (timespec[1] / 10e8)
+            return ts
+
+        @staticmethod
+        def from_stat_time(stat_time):
+            ts = FSTime()
+            ts._value = stat_time
+            return ts
+
+        def approx_secs(self):
+            return self._value
+
+        def secs_nsecs(self):
+            "Return a (s, ns) pair: -1.5s -> (-1, -5**9)."
+            x = math.modf(self._value)
+            return (x[1], x[0] * 10**9)
+
+
 def lutime(path, times):
     if _helpers.utimensat:
-        atime = times[0]
-        mtime = times[1]
+        atime = times[0].to_timespec()
+        mtime = times[1].to_timespec()
         return _helpers.utimensat(_helpers.AT_FDCWD, path, (atime, mtime),
                                   _helpers.AT_SYMLINK_NOFOLLOW)
     else:
@@ -424,14 +498,14 @@ def lutime(path, times):
 
 
 def utime(path, times):
-    atime = times[0]
-    mtime = times[1]
     if _helpers.utimensat:
-        return _helpers.utimensat(_helpers.AT_FDCWD, path, (atime, mtime),
-                                  0)
+        atime = times[0].to_timespec()
+        mtime = times[1].to_timespec()
+        return _helpers.utimensat(_helpers.AT_FDCWD, path, (atime, mtime), 0)
     else:
-        os.utime(path, (atime[0] + atime[1] / 10e9,
-                        mtime[0] + mtime[1] / 10e9))
+        atime = times[0].approx_secs()
+        mtime = times[1].approx_secs()
+        os.utime(path, (atime, mtime))
 
 
 class stat_result():
@@ -450,9 +524,9 @@ def lstat(path):
          result.st_gid,
          result.st_rdev,
          result.st_size,
-         result.st_atime,
-         result.st_mtime,
-         result.st_ctime) = st
+         atime,
+         mtime,
+         ctime) = st
     else:
         st = os.lstat(path)
         result.st_mode = st.st_mode
@@ -463,12 +537,12 @@ def lstat(path):
         result.st_gid = st.st_gid
         result.st_rdev = st.st_rdev
         result.st_size = st.st_size
-        result.st_atime = (math.trunc(st.st_atime),
-                           math.trunc(math.fmod(st.st_atime, 1) * 10**9))
-        result.st_mtime = (math.trunc(st.st_mtime),
-                           math.trunc(math.fmod(st.st_mtime, 1) * 10**9))
-        result.st_ctime = (math.trunc(st.st_ctime),
-                           math.trunc(math.fmod(st.st_ctime, 1) * 10**9))
+        atime = FSTime.from_stat_time(st.st_atime)
+        mtime = FSTime.from_stat_time(st.st_mtime)
+        ctime = FSTime.from_stat_time(st.st_ctime)
+    result.st_atime = FSTime.from_stat_time(atime)
+    result.st_mtime = FSTime.from_stat_time(mtime)
+    result.st_ctime = FSTime.from_stat_time(ctime)
     return result
 
 
index c598701e70d3a0b07ba36afa42715399ed3a2bea..d520f5129a7fb157aace901ce77f1d99b6f6f15a 100644 (file)
@@ -9,7 +9,7 @@ import errno, os, sys, stat, pwd, grp, struct, xattr, posix1e, re
 
 from cStringIO import StringIO
 from bup import vint
-from bup.helpers import add_error, mkdirp, log, utime, lutime, lstat
+from bup.helpers import add_error, mkdirp, log, utime, lutime, lstat, FSTime
 import bup._helpers as _helpers
 
 if _helpers.get_linux_file_attr:
@@ -132,18 +132,6 @@ def _clean_up_extract_path(p):
         return result
 
 
-def _normalize_ts(stamp):
-    # For the purposes of normalization, t = s + ns.
-    s = stamp[0]
-    ns = stamp[1]
-    if ns < 0 or ns >= 10**9:
-        t = (s * 10**9) + ns
-        if t == 0:
-            return (0, 0)
-        return ((t / 10**9), t % 10**9)
-    return stamp
-
-
 # These tags are currently conceptually private to Metadata, and they
 # must be unique, and must *never* be changed.
 _rec_tag_end = 0
@@ -188,9 +176,9 @@ class Metadata:
         self.group = grp.getgrgid(st.st_gid)[0]
 
     def _encode_common(self):
-        atime = _normalize_ts(self.atime)
-        mtime = _normalize_ts(self.mtime)
-        ctime = _normalize_ts(self.ctime)
+        atime = self.atime.to_timespec()
+        mtime = self.mtime.to_timespec()
+        ctime = self.ctime.to_timespec()
         result = vint.pack('VVsVsVvVvVvV',
                            self.mode,
                            self.uid,
@@ -220,21 +208,9 @@ class Metadata:
          mtime_ns,
          self.ctime,
          ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
-        self.atime = (self.atime, atime_ns)
-        self.mtime = (self.mtime, mtime_ns)
-        self.ctime = (self.ctime, ctime_ns)
-        if self.atime[1] >= 10**9:
-            path = ' for ' + self.path if self.path else ''
-            log('bup: warning - normalizing bad atime%s\n' % (path))
-            self.atime = _normalize_ts(self.atime)
-        if self.mtime[1] >= 10**9:
-            path = ' for ' + self.path if self.path else ''
-            log('bup: warning - normalizing bad mtime%s\n' % (path))
-            self.mtime = _normalize_ts(self.mtime)
-        if self.ctime[1] >= 10**9:
-            path = ' for ' + self.path if self.path else ''
-            log('bup: warning - normalizing bad ctime%s\n' % (path))
-            self.ctime = _normalize_ts(self.ctime)
+        self.atime = FSTime.from_timespec((self.atime, atime_ns))
+        self.mtime = FSTime.from_timespec((self.mtime, mtime_ns))
+        self.ctime = FSTime.from_timespec((self.ctime, ctime_ns))
 
     def _create_via_common_rec(self, path, create_symlinks=True):
         # If the path already exists and is a dir, try rmdir.
index c06f5538209c2518ca6fbc42c3983cf765febee1..8fec0f55df91c468629785228fd4564918f214c5 100644 (file)
@@ -1,4 +1,5 @@
-import os
+import os, math
+import bup._helpers as _helpers
 
 from bup.helpers import *
 from wvtest import *
@@ -20,3 +21,56 @@ def test_detect_fakeroot():
         WVPASS(detect_fakeroot())
     else:
         WVPASS(not detect_fakeroot())
+
+
+def _test_fstime():
+    def approx_eq(x, y):
+        return math.fabs(x - y) < 1 / 10e8
+    def ts_eq_ish(x, y):
+        return approx_eq(x[0], y[0]) and approx_eq(x[1], y[1])
+    def fst_eq_ish(x, y):
+        return approx_eq(x.approx_secs(), y.approx_secs())
+    def s_ns_eq_ish(fst, s, ns):
+        (fst_s, fst_ns) = fst.secs_nsecs()
+        return approx_eq(fst_s, s) and approx_eq(fst_ns, ns)
+    from_secs = FSTime.from_secs
+    from_ts = FSTime.from_timespec
+    WVPASS(from_secs(0) == from_secs(0))
+    WVPASS(from_secs(0) < from_secs(1))
+    WVPASS(from_secs(-1) < from_secs(1))
+    WVPASS(from_secs(1) > from_secs(0))
+    WVPASS(from_secs(1) > from_secs(-1))
+
+    WVPASS(fst_eq_ish(from_secs(0), from_ts((0, 0))))
+    WVPASS(fst_eq_ish(from_secs(1), from_ts((1, 0))))
+    WVPASS(fst_eq_ish(from_secs(-1), from_ts((-1, 0))))
+    WVPASS(fst_eq_ish(from_secs(-0.5), from_ts((-1, 10**9 / 2))))
+    WVPASS(fst_eq_ish(from_secs(-1.5), from_ts((-2, 10**9 / 2))))
+    WVPASS(fst_eq_ish(from_secs(1 / 10e8), from_ts((0, 1))))
+    WVPASS(fst_eq_ish(from_secs(-1 / 10e8), from_ts((-1, 10**9 - 1))))
+
+    WVPASS(ts_eq_ish(from_secs(0).to_timespec(), (0, 0)))
+    WVPASS(ts_eq_ish(from_secs(1).to_timespec(), (1, 0)))
+    WVPASS(ts_eq_ish(from_secs(-1).to_timespec(), (-1, 0)))
+    WVPASS(ts_eq_ish(from_secs(-0.5).to_timespec(), (-1, 10**9 / 2)))
+    WVPASS(ts_eq_ish(from_secs(-1.5).to_timespec(), (-2, 10**9 / 2)))
+    WVPASS(ts_eq_ish(from_secs(1 / 10e8).to_timespec(), (0, 1)))
+    WVPASS(ts_eq_ish(from_secs(-1 / 10e8).to_timespec(), (-1, 10**9 - 1)))
+
+    WVPASS(s_ns_eq_ish(from_secs(0), 0, 0))
+    WVPASS(s_ns_eq_ish(from_secs(1), 1, 0))
+    WVPASS(s_ns_eq_ish(from_secs(-1), -1, 0))
+    WVPASS(s_ns_eq_ish(from_secs(-0.5), 0, - 10**9 / 2))
+    WVPASS(s_ns_eq_ish(from_secs(-1.5), -1, - 10**9 / 2))
+    WVPASS(s_ns_eq_ish(from_secs(-1 / 10e8), 0, -1))
+
+@wvtest
+def test_fstime():
+    _test_fstime();
+    if _helpers.lstat: # Also test native python timestamp rep since we can.
+        orig_lstat = _helpers.lstat
+        try:
+            _helpers.lstat = None
+            _test_fstime();
+        finally:
+            _helpers.lstat = orig_lstat
index b09a87e6f9c38af63c1485a558190f902e118046..8aa4f1d199e35d123fe5ed736f7de4b21714e185 100644 (file)
@@ -8,22 +8,6 @@ from bup.helpers import detect_fakeroot
 from wvtest import *
 
 
-@wvtest
-def test__normalize_ts():
-    normalize = metadata._normalize_ts
-    bns = 10**9
-    for ts in ((0, 0), (-1, 0), (0, bns - 1), (-1, bns - 1)):
-        WVPASSEQ(normalize(ts), ts)
-    WVPASSEQ(normalize((0, -1)), (-1, bns - 1))
-    WVPASSEQ(normalize((-1, -1)), (-2, bns - 1))
-    WVPASSEQ(normalize((0, bns)), (1, 0))
-    WVPASSEQ(normalize((0, bns + 1)), (1, 1))
-    WVPASSEQ(normalize((0, -bns)), (-1, 0))
-    WVPASSEQ(normalize((0, -(bns + 1))), (-2, bns - 1))
-    WVPASSEQ(normalize((0, 3 * bns)), (3, 0))
-    WVPASSEQ(normalize((0, -3 * bns)), (-3, 0))
-
-
 @wvtest
 def test_clean_up_archive_path():
     cleanup = metadata._clean_up_path_for_archive