# Copyright (C) 2008-2015 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import errno import fcntl import grp import os import pwd import tempfile import time import tracing import obnamlib # Pylint doesn't see the function defined in _obnam. We silence, for # this module only, the no-member warning. # # pylint: disable=no-member # O_NOATIME is Linux specific: EXTRA_OPEN_FLAGS = getattr(os, "O_NOATIME", 0) class MallocError(obnamlib.ObnamError): msg = 'malloc out of memory while calling {function}' class RootIsNotADirectory(obnamlib.ObnamError): msg = '{baseurl} is not a directory, but a VFS root must be a directory' class LocalFSFile(file): def read(self, amount=-1): offset = self.tell() data = file.read(self, amount) if data: fd = self.fileno() obnamlib._obnam.fadvise_dontneed(fd, offset, len(data)) return data def write(self, data): offset = self.tell() file.write(self, data) fd = self.fileno() obnamlib._obnam.fadvise_dontneed(fd, offset, len(data)) class LocalFS(obnamlib.VirtualFileSystem): """A VFS implementation for local filesystems.""" chunk_size = 1024 * 1024 def __init__(self, baseurl, create=False): tracing.trace('baseurl=%s', baseurl) tracing.trace('create=%s', create) obnamlib.VirtualFileSystem.__init__(self, baseurl) self.reinit(baseurl, create=create) # For checking that we do not unlock something we didn't lock # ourselves. self.our_locks = set() # For testing purposes, allow setting a limit on write operations # after which an exception gets raised. If set to None, no crash. self.crash_limit = None self.crash_counter = 0 # Do we have lchmod? self.got_lchmod = hasattr(os, 'lchmod') def maybe_crash(self): # pragma: no cover if self.crash_limit is not None: self.crash_counter += 1 if self.crash_counter >= self.crash_limit: raise Exception( 'Crashing as requested after %d writes' % self.crash_counter) def reinit(self, baseurl, create=False): # We fake chdir so that it doesn't mess with the caller's # perception of current working directory. This also benefits # unit tests. To do this, we store the baseurl as the cwd. tracing.trace('baseurl=%s', baseurl) tracing.trace('create=%s', create) self.cwd = os.path.abspath(baseurl) if os.path.exists(self.cwd): # pragma: no cover if not os.path.isdir(self.cwd): raise RootIsNotADirectory(baseurl=baseurl) if not self.isdir('.'): if create: tracing.trace('creating %s', baseurl) try: os.mkdir(baseurl) except OSError, e: # pragma: no cover # The directory might have been created concurrently # by someone else! if e.errno != errno.EEXIST: raise else: err = errno.ENOENT raise OSError(err, os.strerror(err), self.cwd) def getcwd(self): return self.cwd def chdir(self, pathname): tracing.trace('LocalFS(%s).chdir(%s)', self.baseurl, pathname) newcwd = os.path.abspath(self.join(pathname)) if not os.path.isdir(newcwd): raise OSError('%s is not a directory' % newcwd) self.cwd = newcwd def lock(self, lockname): tracing.trace('attempting lockname=%s', lockname) try: tempname = self._create_tempfile(lockname) self._rename_temp_to_real(tempname, lockname) except OSError, e: if e.errno == errno.EEXIST: raise obnamlib.LockFail( lock_name=lockname, reason='already exists') else: raise # pragma: no cover tracing.trace('got lockname=%s', lockname) tracing.trace('time=%f' % time.time()) self.our_locks.add(lockname) def _create_tempfile(self, pathname): # pragma: no cover path = self.join(pathname) dirname = os.path.dirname(path) if not os.path.exists(dirname): tracing.trace('os.makedirs(%s)' % dirname) try: os.makedirs(dirname, mode=obnamlib.NEW_DIR_MODE) except OSError as e: # pragma: no cover # This avoids a race condition: another Obnam process # may have created the directory between our check and # creation attempt. If so, we ignore it. As long as # the directory exists, all's good. if e.errno != errno.EEXIST: raise fd, tempname = tempfile.mkstemp(dir=dirname) os.fchmod(fd, obnamlib.NEW_FILE_MODE) os.close(fd) f = self.open(tempname, 'wb') f.close() return tempname def _rename_temp_to_real(self, tempname, pathname): # pragma: no cover # This is tricky. We need to at least try to support NFS, and # various filesystems that do not support hardlinks. On NFS, # creating a file with O_EXCL is not guaranteed to be atomic, # but adding a link with link(2) is. However, that doesn't work # on VFAT, for example. So we try do both: first create a # temporary file with a name guaranteed to not be a name we # want to use, and then we rename it using link(2) and remove(2). # If that fails, we try to create the target file with O_EXCL # and rename the temporary file to that. This is still not 100% # reliable: someone could be mounting VFAT across NFS, for # example, but it's the best we can do. If this paragraph is # wrong, tell the authors. tracing.trace('tempname=%r', tempname) tracing.trace('pathname=%r', pathname) path = self.join(pathname) tracing.trace('path=%r', path) # Try link(2) for creating target file. try: os.link(tempname, path) except OSError: pass else: os.remove(tempname) tracing.trace('link+remove worked') return # Nope, didn't work. Now try with O_EXCL instead. try: fd = os.open( path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, obnamlib.NEW_FILE_MODE) os.close(fd) os.rename(tempname, path) except OSError: # Give up. os.remove(tempname) raise tracing.trace('O_EXCL+rename worked') def unlock(self, lockname): tracing.trace('lockname=%s', lockname) assert lockname in self.our_locks self.remove(lockname) self.our_locks.remove(lockname) tracing.trace('time=%f' % time.time()) def join(self, pathname): return os.path.join(self.cwd, pathname) def remove(self, pathname): tracing.trace('remove %s', pathname) os.remove(self.join(pathname)) self.maybe_crash() def rename(self, old, new): tracing.trace('rename %s %s', old, new) os.rename(self.join(old), self.join(new)) self.maybe_crash() def lstat(self, pathname): (ret, dev, ino, mode, nlink, uid, gid, rdev, size, blksize, blocks, atime_sec, atime_nsec, mtime_sec, mtime_nsec, ctime_sec, ctime_nsec) = obnamlib._obnam.lstat(self.join(pathname)) if ret != 0: raise OSError(ret, os.strerror(ret), pathname) return obnamlib.Metadata( st_dev=dev, st_ino=ino, st_mode=mode, st_nlink=nlink, st_uid=uid, st_gid=gid, st_rdev=rdev, st_size=size, st_blksize=blksize, st_blocks=blocks, st_atime_sec=atime_sec, st_atime_nsec=atime_nsec, st_mtime_sec=mtime_sec, st_mtime_nsec=mtime_nsec, st_ctime_sec=ctime_sec, st_ctime_nsec=ctime_nsec ) def get_username(self, uid): return pwd.getpwuid(uid)[0] def get_groupname(self, gid): return grp.getgrgid(gid)[0] def llistxattr(self, filename): # pragma: no cover ret = obnamlib._obnam.llistxattr(self.join(filename)) if ret is None: raise MallocError(function='llistxattr') if isinstance(ret, int): raise OSError(ret, os.strerror(ret), filename) return [s for s in ret.split('\0') if s] def lgetxattr(self, filename, attrname): # pragma: no cover ret = obnamlib._obnam.lgetxattr(self.join(filename), attrname) if ret is None: raise MallocError(function='llistxattr') if isinstance(ret, int): raise OSError(ret, os.strerror(ret), filename) return ret def lsetxattr(self, filename, attrname, attrvalue): # pragma: no cover ret = obnamlib._obnam.lsetxattr(self.join(filename), attrname, attrvalue) if ret != 0: raise OSError(ret, os.strerror(ret), filename) def lchown(self, pathname, uid, gid): # pragma: no cover tracing.trace('lchown %s %d %d', pathname, uid, gid) os.lchown(self.join(pathname), uid, gid) # This method is excluded from test coverage because the platform # either has lchmod or doesn't, and accordingly either branch of # the if statement is taken, and the other branch shows up as not # being tested by the unit tests. def chmod_symlink(self, pathname, mode): # pragma: no cover tracing.trace('chmod_symlink %s %o', pathname, mode) if self.got_lchmod: lchmod = getattr(os, 'lchmod') lchmod(self.join(pathname), mode) else: self.lstat(pathname) def chmod_not_symlink(self, pathname, mode): tracing.trace('chmod_not_symlink %s %o', pathname, mode) os.chmod(self.join(pathname), mode) def lutimes(self, pathname, atime_sec, atime_nsec, mtime_sec, mtime_nsec): assert atime_sec is not None assert atime_nsec is not None assert mtime_sec is not None assert mtime_nsec is not None ret = obnamlib._obnam.utimensat(self.join(pathname), atime_sec, atime_nsec, mtime_sec, mtime_nsec) if ret != 0: raise OSError(ret, os.strerror(ret), pathname) def link(self, existing, new): tracing.trace('existing=%s', existing) tracing.trace('new=%s', new) os.link(self.join(existing), self.join(new)) self.maybe_crash() def readlink(self, pathname): return os.readlink(self.join(pathname)) def symlink(self, existing, new): tracing.trace('existing=%s', existing) tracing.trace('new=%s', new) os.symlink(existing, self.join(new)) self.maybe_crash() def open(self, pathname, mode, bufsize=None): tracing.trace('pathname=%s', pathname) tracing.trace('mode=%s', mode) f = LocalFSFile(self.join(pathname), mode) tracing.trace('opened %s', pathname) try: flags = fcntl.fcntl(f.fileno(), fcntl.F_GETFL) flags |= EXTRA_OPEN_FLAGS fcntl.fcntl(f.fileno(), fcntl.F_SETFL, flags) except IOError, e: # pragma: no cover tracing.trace('fcntl F_SETFL failed: %s', repr(e)) return f # ignore any problems setting flags tracing.trace('returning ok') return f def exists(self, pathname): return os.path.exists(self.join(pathname)) def isdir(self, pathname): return os.path.isdir(self.join(pathname)) def mknod(self, pathname, mode): tracing.trace('pathname=%s', pathname) tracing.trace('mode=%o', mode) os.mknod(self.join(pathname), mode) def mkdir(self, pathname, mode=obnamlib.NEW_DIR_MODE): tracing.trace('mkdir %s', pathname) os.mkdir(self.join(pathname), obnamlib.NEW_DIR_MODE) self.maybe_crash() def makedirs(self, pathname): tracing.trace('makedirs %s', pathname) os.makedirs(self.join(pathname), obnamlib.NEW_DIR_MODE) self.maybe_crash() def rmdir(self, pathname): tracing.trace('rmdir %s', pathname) os.rmdir(self.join(pathname)) self.maybe_crash() def cat(self, pathname): tracing.trace('pathname=%s' % pathname) pathname = self.join(pathname) f = self.open(pathname, 'rb') chunks = [] while True: chunk = f.read(self.chunk_size) if not chunk: break chunks.append(chunk) self.bytes_read += len(chunk) f.close() data = ''.join(chunks) return data def write_file(self, pathname, contents): # pragma: no cover tempname = self._create_tempfile(pathname) f = self.open(tempname, 'wb') f.write(contents) f.close() self.bytes_written += len(contents) path = self.join(pathname) self._rename_temp_to_real(tempname, path) self.maybe_crash() def overwrite_file(self, pathname, contents): tracing.trace('overwrite_file %s', pathname) tempname = self._create_tempfile(pathname) f = self.open(tempname, 'wb') f.write(contents) f.close() self.bytes_written += len(contents) path = self.join(pathname) os.rename(tempname, path) self.maybe_crash() def listdir(self, dirname): return os.listdir(self.join(dirname)) def listdir2(self, dirname): result = [] for name in self.listdir(dirname): try: st = self.lstat(os.path.join(dirname, name)) except OSError, e: # pragma: no cover st = e ino = -1 else: ino = st.st_ino result.append((ino, name, st)) # We sort things in inode order, for speed when doing name lookups # when backing up. result.sort() return [(name, st) for ino, name, st in result]