summaryrefslogtreecommitdiff
path: root/src/buildstream/_fuse/hardlinks.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/buildstream/_fuse/hardlinks.py')
-rw-r--r--src/buildstream/_fuse/hardlinks.py218
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)