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))
134 """Get the user's login name."""
139 _username = pwd.getpwuid(uid)[0]
141 _username = 'user%d' % uid
147 """Get the user's full name."""
149 if not _userfullname:
152 _userfullname = pwd.getpwuid(uid)[4].split(',')[0]
154 _userfullname = 'user%d' % uid
160 """Get the FQDN of this machine."""
163 _hostname = socket.getfqdn()
167 _resource_path = None
168 def resource_path(subdir=''):
169 global _resource_path
170 if not _resource_path:
171 _resource_path = os.environ.get('BUP_RESOURCE_PATH') or '.'
172 return os.path.join(_resource_path, subdir)
174 class NotOk(Exception):
178 """A helper class for bup's client-server protocol."""
179 def __init__(self, inp, outp):
183 def read(self, size):
184 """Read 'size' bytes from input stream."""
186 return self.inp.read(size)
189 """Read from input stream until a newline is found."""
191 return self.inp.readline()
193 def write(self, data):
194 """Write 'data' to output stream."""
195 #log('%d writing: %d bytes\n' % (os.getpid(), len(data)))
196 self.outp.write(data)
199 """Return true if input stream is readable."""
200 [rl, wl, xl] = select.select([self.inp.fileno()], [], [], 0)
202 assert(rl[0] == self.inp.fileno())
208 """Indicate end of output from last sent command."""
212 """Indicate server error to the client."""
213 s = re.sub(r'\s+', ' ', str(s))
214 self.write('\nerror %s\n' % s)
216 def _check_ok(self, onempty):
219 for rl in linereader(self.inp):
220 #log('%d got line: %r\n' % (os.getpid(), rl))
221 if not rl: # empty line
225 elif rl.startswith('error '):
226 #log('client: error: %s\n' % rl[6:])
230 raise Exception('server exited unexpectedly; see errors above')
232 def drain_and_check_ok(self):
233 """Remove all data for the current command from input stream."""
236 return self._check_ok(onempty)
239 """Verify that server action completed successfully."""
241 raise Exception('expected "ok", got %r' % rl)
242 return self._check_ok(onempty)
246 """Generate a list of input lines from 'f' without terminating newlines."""
254 def chunkyreader(f, count = None):
255 """Generate a list of chunks of data read from 'f'.
257 If count is None, read until EOF is reached.
259 If count is a positive integer, read 'count' bytes from 'f'. If EOF is
260 reached while reading, raise IOError.
264 b = f.read(min(count, 65536))
266 raise IOError('EOF with %d bytes remaining' % count)
277 """Append "/" to 's' if it doesn't aleady end in "/"."""
278 if s and not s.endswith('/'):
284 def _mmap_do(f, sz, flags, prot):
286 st = os.fstat(f.fileno())
288 map = mmap.mmap(f.fileno(), sz, flags, prot)
289 f.close() # map will persist beyond file close
293 def mmap_read(f, sz = 0):
294 """Create a read-only memory mapped region on file 'f'.
296 If sz is 0, the region will cover the entire file.
298 return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ)
301 def mmap_readwrite(f, sz = 0):
302 """Create a read-write memory mapped region on file 'f'.
304 If sz is 0, the region will cover the entire file.
306 return _mmap_do(f, sz, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE)
310 """Parse data size information into a float number.
312 Here are some examples of conversions:
313 199.2k means 203981 bytes
314 1GB means 1073741824 bytes
315 2.1 tb means 2199023255552 bytes
317 g = re.match(r'([-+\d.e]+)\s*(\w*)', str(s))
319 raise ValueError("can't parse %r as a number" % s)
320 (val, unit) = g.groups()
323 if unit in ['t', 'tb']:
324 mult = 1024*1024*1024*1024
325 elif unit in ['g', 'gb']:
326 mult = 1024*1024*1024
327 elif unit in ['m', 'mb']:
329 elif unit in ['k', 'kb']:
331 elif unit in ['', 'b']:
334 raise ValueError("invalid unit %r in number %r" % (unit, s))
339 """Count the number of elements in an iterator. (consumes the iterator)"""
340 return reduce(lambda x,y: x+1, l)
345 """Append an error message to the list of saved errors.
347 Once processing is able to stop and output the errors, the saved errors are
348 accessible in the module variable helpers.saved_errors.
350 saved_errors.append(e)
353 istty = os.isatty(2) or atoi(os.environ.get('BUP_FORCE_TTY'))
355 """Calls log(s) if stderr is a TTY. Does nothing otherwise."""
361 """Replace the default exception handler for KeyboardInterrupt (Ctrl-C).
363 The new exception handler will make sure that bup will exit without an ugly
364 stacktrace when Ctrl-C is hit.
366 oldhook = sys.excepthook
367 def newhook(exctype, value, traceback):
368 if exctype == KeyboardInterrupt:
369 log('Interrupted.\n')
371 return oldhook(exctype, value, traceback)
372 sys.excepthook = newhook
375 def columnate(l, prefix):
376 """Format elements of 'l' in columns with 'prefix' leading each line.
378 The number of columns is determined automatically based on the string
384 clen = max(len(s) for s in l)
385 ncols = (tty_width() - len(prefix)) / (clen + 2)
390 while len(l) % ncols:
393 for s in range(0, len(l), rows):
394 cols.append(l[s:s+rows])
396 for row in zip(*cols):
397 out += prefix + ''.join(('%-*s' % (clen+2, s)) for s in row) + '\n'
400 def parse_date_or_fatal(str, fatal):
401 """Parses the given date or calls Option.fatal().
402 For now we expect a string that contains a float."""
405 except ValueError, e:
406 raise fatal('invalid date format (should be a float): %r' % e)
411 def lutime(path, times):
412 if _helpers.utimensat:
415 return _helpers.utimensat(_helpers.AT_FDCWD, path, (atime, mtime),
416 _helpers.AT_SYMLINK_NOFOLLOW)
421 def utime(path, times):
424 if _helpers.utimensat:
425 return _helpers.utimensat(_helpers.AT_FDCWD, path, (atime, mtime),
428 os.utime(path, (atime[0] + atime[1] / 10e9,
429 mtime[0] + mtime[1] / 10e9))
437 result = stat_result()
439 st = _helpers.lstat(path)
450 result.st_ctime) = st
453 result.st_mode = st.st_mode
454 result.st_ino = st.st_ino
455 result.st_dev = st.st_dev
456 result.st_nlink = st.st_nlink
457 result.st_uid = st.st_uid
458 result.st_gid = st.st_gid
459 result.st_rdev = st.st_rdev
460 result.st_size = st.st_size
461 result.st_atime = (math.trunc(st.st_atime),
462 math.trunc(math.fmod(st.st_atime, 1) * 10**9))
463 result.st_mtime = (math.trunc(st.st_mtime),
464 math.trunc(math.fmod(st.st_mtime, 1) * 10**9))
465 result.st_ctime = (math.trunc(st.st_ctime),
466 math.trunc(math.fmod(st.st_ctime, 1) * 10**9))
470 # hashlib is only available in python 2.5 or higher, but the 'sha' module
471 # produces a DeprecationWarning in python 2.6 or higher. We want to support
472 # python 2.4 and above without any stupid warnings, so let's try using hashlib
473 # first, and downgrade if it fails.
484 """Format bup's version date string for output."""
485 return _version.DATE.split(' ')[0]
487 def version_commit():
488 """Get the commit hash of bup's current version."""
489 return _version.COMMIT
492 """Format bup's version tag (the official version number).
494 When generated from a commit other than one pointed to with a tag, the
495 returned string will be "unknown-" followed by the first seven positions of
498 names = _version.NAMES.strip()
499 assert(names[0] == '(')
500 assert(names[-1] == ')')
502 l = [n.strip() for n in names.split(',')]
504 if n.startswith('tag: bup-'):
506 return 'unknown-%s' % _version.COMMIT[:7]