def copy(self, dest, skip_if_older=True): assert isinstance(dest, basestring) if not hasattr(os, 'link'): return super(HardlinkFile, self).copy( dest, skip_if_older=skip_if_older ) try: path_st = os.stat(self.path) except OSError as e: if e.errno == errno.ENOENT: raise ErrorMessage('Hard link target path does not exist: %s' % self.path) else: raise st = None try: st = os.lstat(dest) except OSError as e: if e.errno != errno.ENOENT: raise if st: # The dest already points to the right place. if st.st_dev == path_st.st_dev and st.st_ino == path_st.st_ino: return False # The dest exists and it points to the wrong place os.remove(dest) # At this point, either the dest used to exist and we just deleted it, # or it never existed. We can now safely create the hard link. try: os.link(self.path, dest) except OSError: # If we can't hard link, fall back to copying return super(HardlinkFile, self).copy( dest, skip_if_older=skip_if_older ) return True
def copy(self, dest, skip_if_older=True): assert isinstance(dest, basestring) # The logic in this function is complicated by the fact that symlinks # aren't universally supported. So, where symlinks aren't supported, we # fall back to file copying. Keep in mind that symlink support is # per-filesystem, not per-OS. # Handle the simple case where symlinks are definitely not supported by # falling back to file copy. if not hasattr(os, 'symlink'): return File.copy(self, dest, skip_if_older=skip_if_older) # Always verify the symlink target path exists. if not os.path.exists(self.path): raise ErrorMessage('Symlink target path does not exist: %s' % self.path) st = None try: st = os.lstat(dest) except OSError as ose: if ose.errno != errno.ENOENT: raise # If the dest is a symlink pointing to us, we have nothing to do. # If it's the wrong symlink, the filesystem must support symlinks, # so we replace with a proper symlink. if st and stat.S_ISLNK(st.st_mode): link = os.readlink(dest) if link == self.path: return False os.remove(dest) os.symlink(self.path, dest) return True # If the destination doesn't exist, we try to create a symlink. If that # fails, we fall back to copy code. if not st: try: os.symlink(self.path, dest) return True except OSError: return File.copy(self, dest, skip_if_older=skip_if_older) # Now the complicated part. If the destination exists, we could be # replacing a file with a symlink. Or, the filesystem may not support # symlinks. We want to minimize I/O overhead for performance reasons, # so we keep the existing destination file around as long as possible. # A lot of the system calls would be eliminated if we cached whether # symlinks are supported. However, even if we performed a single # up-front test of whether the root of the destination directory # supports symlinks, there's no guarantee that all operations for that # dest (or source) would be on the same filesystem and would support # symlinks. # # Our strategy is to attempt to create a new symlink with a random # name. If that fails, we fall back to copy mode. If that works, we # remove the old destination and move the newly-created symlink into # its place. temp_dest = os.path.join(os.path.dirname(dest), str(uuid.uuid4())) try: os.symlink(self.path, temp_dest) # TODO Figure out exactly how symlink creation fails and only trap # that. except EnvironmentError: return File.copy(self, dest, skip_if_older=skip_if_older) # If removing the original file fails, don't forget to clean up the # temporary symlink. try: os.remove(dest) except EnvironmentError: os.remove(temp_dest) raise os.rename(temp_dest, dest) return True