1 """Helper functions and classes for bup."""
2 import sys, os, pwd, subprocess, errno, socket, select, mmap, stat, re
3 from bup import _version
4 import bup._helpers as _helpers
6 # This function should really be in helpers, not in bup.options. But we
7 # want options.py to be standalone so people can include it in other projects.
8 from bup.options import _tty_width
13 """Convert the string 's' to an integer. Return 0 if s is not a number."""
21 """Convert the string 's' to a float. Return 0 if s is not a number."""
23 return float(s or '0')
28 buglvl = atoi(os.environ.get('BUP_DEBUG', 0))
31 # Write (blockingly) to sockets that may or may not be in blocking mode.
32 # We need this because our stderr is sometimes eaten by subprocesses
33 # (probably ssh) that sometimes make it nonblocking, if only temporarily,
34 # leading to race conditions. Ick. We'll do it the hard way.
35 def _hard_write(fd, buf):
37 (r,w,x) = select.select([], [fd], [], None)
39 raise IOError('select(fd) returned without being writable')
41 sz = os.write(fd, buf)
43 if e.errno != errno.EAGAIN:
49 """Print a log message to stderr."""
51 _hard_write(sys.stderr.fileno(), s)
64 def mkdirp(d, mode=None):
65 """Recursively create directories on path 'd'.
67 Unlike os.makedirs(), it doesn't raise an exception if the last element of
68 the path already exists.
76 if e.errno == errno.EEXIST:
83 """Get the next item from an iterator, None if we reached the end."""
91 """Delete a file at path 'f' if it currently exists.
93 Unlike os.unlink(), does not throw an exception if the file didn't already
99 if e.errno == errno.ENOENT:
100 pass # it doesn't exist, that's what you asked for
104 """Run a subprocess and return its output."""
105 p = subprocess.Popen(argv, stdout=subprocess.PIPE)
112 """Get the absolute path of a file.
114 Behaves like os.path.realpath, but doesn't follow a symlink for the last
115 element. (ie. if 'p' itself is a symlink, this one won't follow it, but it
116 will follow symlinks in p's directory)
122 if st and stat.S_ISLNK(st.st_mode):
123 (dir, name) = os.path.split(p)
124 dir = os.path.realpath(dir)
125 out = os.path.join(dir, name)
127 out = os.path.realpath(p)
128 #log('realpathing:%r,%r\n' % (p, out))
132 def detect_fakeroot():
133 "Return True if we appear to be running under fakeroot."
134 return os.getenv("FAKEROOTKEY") != None
139 """Get the user's login name."""
144 _username = pwd.getpwuid(uid)[0]
146 _username = 'user%d' % uid
152 """Get the user's full name."""
154 if not _userfullname:
157 _userfullname = pwd.getpwuid(uid)[4].split(',')[0]
159 _userfullname = 'user%d' % uid
165 """Get the FQDN of this machine."""
168 _hostname = socket.getfqdn()
172 _resource_path = None
173 def resource_path(subdir=''):
174 global _resource_path
175 if not _resource_path:
176 _resource_path = os.environ.get('BUP_RESOURCE_PATH') or '.'
177 return os.path.join(_resource_path, subdir)
179 class NotOk(Exception):
183 """A helper class for bup's client-server protocol."""
184 def __init__(self, inp, outp):
188 def read(self, size):
189 """Read 'size' bytes from input stream."""
191 return self.inp.read(size)
194 """Read from input stream until a newline is found."""
196 return self.inp.readline()
198 def write(self, data):
199 """Write 'data' to output stream."""
200 #log('%d writing: %d bytes\n' % (os.getpid(), len(data)))
201 self.outp.write(data)
204 """Return true if input stream is readable."""
205 [rl, wl, xl] = select.select([self.inp.fileno()], [], [], 0)
207 assert(rl[0] == self.inp.fileno())
213 """Indicate end of output from last sent command."""
217 """Indicate server error to the client."""
218 s = re.sub(r'\s+', ' ', str(s))
219 self.write('\nerror %s\n' % s)
221 def _check_ok(self, onempty):
224 for rl in linereader(self.inp):
225 #log('%d got line: %r\n' % (os.getpid(), rl))
226 if not rl: # empty line
230 elif rl.startswith('error '):
231 #log('client: error: %s\n' % rl[6:])
235 raise Exception('server exited unexpectedly; see errors above')
237 def drain_and_check_ok(self):
238 """Remove all data for the current command from input stream."""
241 return self._check_ok(onempty)
244 """Verify that server action completed successfully."""
246 raise Exception('expected "ok", got %r' % rl)
247 return self._check_ok(onempty)
251 """Generate a list of input lines from 'f' without terminating newlines."""
259 def chunkyreader(f, count = None):
260 """Generate a list of chunks of data read from 'f'.
262 If count is None, read until EOF is reached.
264 If count is a positive integer, read 'count' bytes from 'f'. If EOF is
265 reached while reading, raise IOError.
269 b = f.read(min(count, 65536))
271 raise IOError('EOF with %d bytes remaining' % count)
282 """Append "/" to 's' if it doesn't aleady end in "/"."""
283 if s and not s.endswith('/'):
289 def _mmap_do(f, sz, flags, prot):
291 st = os.fstat(f.fileno())
293 map = mmap.mmap(f.fileno(), sz, flags, prot)
294 f.close() # map will persist beyond file close
298 def mmap_read(f, sz = 0):
299 """Create a read-only memory mapped region on file 'f'.
301 If sz is 0, the region will cover the entire file.
303 return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ)
306 def mmap_readwrite(f, sz = 0):
307 """Create a read-write memory mapped region on file 'f'.
309 If sz is 0, the region will cover the entire file.
311 return _mmap_do(f, sz, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE)
315 """Parse data size information into a float number.
317 Here are some examples of conversions:
318 199.2k means 203981 bytes
319 1GB means 1073741824 bytes
320 2.1 tb means 2199023255552 bytes
322 g = re.match(r'([-+\d.e]+)\s*(\w*)', str(s))
324 raise ValueError("can't parse %r as a number" % s)
325 (val, unit) = g.groups()
328 if unit in ['t', 'tb']:
329 mult = 1024*1024*1024*1024
330 elif unit in ['g', 'gb']:
331 mult = 1024*1024*1024
332 elif unit in ['m', 'mb']:
334 elif unit in ['k', 'kb']:
336 elif unit in ['', 'b']:
339 raise ValueError("invalid unit %r in number %r" % (unit, s))
344 """Count the number of elements in an iterator. (consumes the iterator)"""
345 return reduce(lambda x,y: x+1, l)
350 """Append an error message to the list of saved errors.
352 Once processing is able to stop and output the errors, the saved errors are
353 accessible in the module variable helpers.saved_errors.
355 saved_errors.append(e)
358 istty = os.isatty(2) or atoi(os.environ.get('BUP_FORCE_TTY'))
360 """Calls log(s) if stderr is a TTY. Does nothing otherwise."""
366 """Replace the default exception handler for KeyboardInterrupt (Ctrl-C).
368 The new exception handler will make sure that bup will exit without an ugly
369 stacktrace when Ctrl-C is hit.
371 oldhook = sys.excepthook
372 def newhook(exctype, value, traceback):
373 if exctype == KeyboardInterrupt:
374 log('Interrupted.\n')
376 return oldhook(exctype, value, traceback)
377 sys.excepthook = newhook
380 def columnate(l, prefix):
381 """Format elements of 'l' in columns with 'prefix' leading each line.
383 The number of columns is determined automatically based on the string
389 clen = max(len(s) for s in l)
390 ncols = (tty_width() - len(prefix)) / (clen + 2)
395 while len(l) % ncols:
398 for s in range(0, len(l), rows):
399 cols.append(l[s:s+rows])
401 for row in zip(*cols):
402 out += prefix + ''.join(('%-*s' % (clen+2, s)) for s in row) + '\n'
405 def parse_date_or_fatal(str, fatal):
406 """Parses the given date or calls Option.fatal().
407 For now we expect a string that contains a float."""
410 except ValueError, e:
411 raise fatal('invalid date format (should be a float): %r' % e)
417 # Class to represent filesystem timestamps. Use integer
418 # nanoseconds on platforms where we have the higher resolution
419 # lstat. Use the native python stat representation (floating
420 # point seconds) otherwise.
422 def __cmp__(self, x):
423 return self._value.__cmp__(x._value)
425 def to_timespec(self):
426 """Return (s, ns) where ns is always non-negative
427 and t = s + ns / 10e8""" # metadata record rep (and libc rep)
428 s_ns = self.secs_nsecs()
429 if s_ns[0] > 0 or s_ns[1] >= 0:
431 return (s_ns[0] - 1, 10**9 + s_ns[1]) # ns is negative
433 if _helpers.lstat: # Use integer nanoseconds.
438 ts._value = int(secs * 10**9)
442 def from_timespec(timespec):
444 ts._value = timespec[0] * 10**9 + timespec[1]
448 def from_stat_time(stat_time):
449 return FSTime.from_timespec(stat_time)
451 def approx_secs(self):
452 return self._value / 10e8;
454 def secs_nsecs(self):
455 "Return a (s, ns) pair: -1.5s -> (-1, -10**9 / 2)."
457 return (self._value / 10**9, self._value % 10**9)
458 abs_val = -self._value
459 return (- (abs_val / 10**9), - (abs_val % 10**9))
461 else: # Use python default floating-point seconds.
470 def from_timespec(timespec):
472 ts._value = timespec[0] + (timespec[1] / 10e8)
476 def from_stat_time(stat_time):
478 ts._value = stat_time
481 def approx_secs(self):
484 def secs_nsecs(self):
485 "Return a (s, ns) pair: -1.5s -> (-1, -5**9)."
486 x = math.modf(self._value)
487 return (x[1], x[0] * 10**9)
490 def lutime(path, times):
491 if _helpers.utimensat:
492 atime = times[0].to_timespec()
493 mtime = times[1].to_timespec()
494 return _helpers.utimensat(_helpers.AT_FDCWD, path, (atime, mtime),
495 _helpers.AT_SYMLINK_NOFOLLOW)
500 def utime(path, times):
501 if _helpers.utimensat:
502 atime = times[0].to_timespec()
503 mtime = times[1].to_timespec()
504 return _helpers.utimensat(_helpers.AT_FDCWD, path, (atime, mtime), 0)
506 atime = times[0].approx_secs()
507 mtime = times[1].approx_secs()
508 os.utime(path, (atime, mtime))
514 def from_stat_rep(st):
515 result = stat_result()
516 if _helpers._have_ns_fs_timestamps:
529 result.st_mode = st.st_mode
530 result.st_ino = st.st_ino
531 result.st_dev = st.st_dev
532 result.st_nlink = st.st_nlink
533 result.st_uid = st.st_uid
534 result.st_gid = st.st_gid
535 result.st_rdev = st.st_rdev
536 result.st_size = st.st_size
537 atime = FSTime.from_stat_time(st.st_atime)
538 mtime = FSTime.from_stat_time(st.st_mtime)
539 ctime = FSTime.from_stat_time(st.st_ctime)
540 result.st_atime = FSTime.from_stat_time(atime)
541 result.st_mtime = FSTime.from_stat_time(mtime)
542 result.st_ctime = FSTime.from_stat_time(ctime)
548 st = _helpers.fstat(path)
551 return stat_result.from_stat_rep(st)
556 st = _helpers.lstat(path)
559 return stat_result.from_stat_rep(st)
562 # hashlib is only available in python 2.5 or higher, but the 'sha' module
563 # produces a DeprecationWarning in python 2.6 or higher. We want to support
564 # python 2.4 and above without any stupid warnings, so let's try using hashlib
565 # first, and downgrade if it fails.
576 """Format bup's version date string for output."""
577 return _version.DATE.split(' ')[0]
579 def version_commit():
580 """Get the commit hash of bup's current version."""
581 return _version.COMMIT
584 """Format bup's version tag (the official version number).
586 When generated from a commit other than one pointed to with a tag, the
587 returned string will be "unknown-" followed by the first seven positions of
590 names = _version.NAMES.strip()
591 assert(names[0] == '(')
592 assert(names[-1] == ')')
594 l = [n.strip() for n in names.split(',')]
596 if n.startswith('tag: bup-'):
598 return 'unknown-%s' % _version.COMMIT[:7]