diff options
Diffstat (limited to 'src/buildstream/_fuse/hardlinks.py')
-rw-r--r-- | src/buildstream/_fuse/hardlinks.py | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/src/buildstream/_fuse/hardlinks.py b/src/buildstream/_fuse/hardlinks.py new file mode 100644 index 000000000..ff2e81eea --- /dev/null +++ b/src/buildstream/_fuse/hardlinks.py @@ -0,0 +1,218 @@ +# +# Copyright (C) 2016 Stavros Korokithakis +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> +# +# The filesystem operations implementation here is based +# on some example code written by Stavros Korokithakis. + +import errno +import os +import shutil +import stat +import tempfile + +from .fuse import FuseOSError, Operations + +from .mount import Mount + + +# SafeHardlinks() +# +# A FUSE mount which implements a copy on write hardlink experience. +# +# Args: +# root (str): The underlying filesystem path to mirror +# tmp (str): A directory on the same filesystem for creating temp files +# +class SafeHardlinks(Mount): + + def __init__(self, directory, tempdir, fuse_mount_options=None): + self.directory = directory + self.tempdir = tempdir + if fuse_mount_options is None: + fuse_mount_options = {} + super().__init__(fuse_mount_options=fuse_mount_options) + + def create_operations(self): + return SafeHardlinkOps(self.directory, self.tempdir) + + +# SafeHardlinkOps() +# +# The actual FUSE Operations implementation below. +# +class SafeHardlinkOps(Operations): + + def __init__(self, root, tmp): + self.root = root + self.tmp = tmp + + def _full_path(self, partial): + if partial.startswith("/"): + partial = partial[1:] + path = os.path.join(self.root, partial) + return path + + def _ensure_copy(self, full_path): + try: + # Follow symbolic links manually here + real_path = os.path.realpath(full_path) + file_stat = os.stat(real_path) + + # Dont bother with files that cannot be hardlinked, oddly it + # directories actually usually have st_nlink > 1 so just avoid + # that. + # + # We already wont get symlinks here, and stat will throw + # the FileNotFoundError below if a followed symlink did not exist. + # + if not stat.S_ISDIR(file_stat.st_mode) and file_stat.st_nlink > 1: + with tempfile.TemporaryDirectory(dir=self.tmp) as tempdir: + basename = os.path.basename(real_path) + temp_path = os.path.join(tempdir, basename) + + # First copy, then unlink origin and rename + shutil.copy2(real_path, temp_path) + os.unlink(real_path) + os.rename(temp_path, real_path) + + except FileNotFoundError: + # This doesnt exist yet, assume we're about to create it + # so it's not a problem. + pass + + ########################################################### + # Fuse Methods # + ########################################################### + def access(self, path, mode): + full_path = self._full_path(path) + if not os.access(full_path, mode): + raise FuseOSError(errno.EACCES) + + def chmod(self, path, mode): + full_path = self._full_path(path) + + # Ensure copies on chmod + self._ensure_copy(full_path) + return os.chmod(full_path, mode) + + def chown(self, path, uid, gid): + full_path = self._full_path(path) + + # Ensure copies on chown + self._ensure_copy(full_path) + return os.chown(full_path, uid, gid) + + def getattr(self, path, fh=None): + full_path = self._full_path(path) + st = os.lstat(full_path) + return dict((key, getattr(st, key)) for key in ( + 'st_atime', 'st_ctime', 'st_gid', 'st_mode', + 'st_mtime', 'st_nlink', 'st_size', 'st_uid', 'st_rdev')) + + def readdir(self, path, fh): + full_path = self._full_path(path) + + dirents = ['.', '..'] + if os.path.isdir(full_path): + dirents.extend(os.listdir(full_path)) + for r in dirents: + yield r + + def readlink(self, path): + pathname = os.readlink(self._full_path(path)) + if pathname.startswith("/"): + # Path name is absolute, sanitize it. + return os.path.relpath(pathname, self.root) + else: + return pathname + + def mknod(self, path, mode, dev): + return os.mknod(self._full_path(path), mode, dev) + + def rmdir(self, path): + full_path = self._full_path(path) + return os.rmdir(full_path) + + def mkdir(self, path, mode): + return os.mkdir(self._full_path(path), mode) + + def statfs(self, path): + full_path = self._full_path(path) + stv = os.statvfs(full_path) + return dict((key, getattr(stv, key)) for key in ( + 'f_bavail', 'f_bfree', 'f_blocks', 'f_bsize', 'f_favail', + 'f_ffree', 'f_files', 'f_flag', 'f_frsize', 'f_namemax')) + + def unlink(self, path): + return os.unlink(self._full_path(path)) + + def symlink(self, name, target): + return os.symlink(target, self._full_path(name)) + + def rename(self, old, new): + return os.rename(self._full_path(old), self._full_path(new)) + + def link(self, target, name): + + # When creating a hard link here, should we ensure the original + # file is not a hardlink itself first ? + # + return os.link(self._full_path(name), self._full_path(target)) + + def utimens(self, path, times=None): + return os.utime(self._full_path(path), times) + + def open(self, path, flags): + full_path = self._full_path(path) + + # If we're opening for writing, ensure it's a copy first + if flags & os.O_WRONLY or flags & os.O_RDWR: + self._ensure_copy(full_path) + + return os.open(full_path, flags) + + def create(self, path, mode, flags): + full_path = self._full_path(path) + + # If it already exists, ensure it's a copy first + self._ensure_copy(full_path) + return os.open(full_path, flags, mode) + + def read(self, path, length, offset, fh): + os.lseek(fh, offset, os.SEEK_SET) + return os.read(fh, length) + + def write(self, path, buf, offset, fh): + os.lseek(fh, offset, os.SEEK_SET) + return os.write(fh, buf) + + def truncate(self, path, length, fh=None): + full_path = self._full_path(path) + with open(full_path, 'r+') as f: + f.truncate(length) + + def flush(self, path, fh): + return os.fsync(fh) + + def release(self, path, fh): + return os.close(fh) + + def fsync(self, path, fdatasync, fh): + return self.flush(path, fh) |