sirosen/SALVE

View on GitHub
salve/filesys/abstract.py

Summary

Maintainability
A
35 mins
Test Coverage
import abc
from contextlib import contextmanager

from salve import Enum, with_metaclass
from .access import access_codes


class Filesys(with_metaclass(abc.ABCMeta)):
    """
    This defines the properties of the filesystem layer, through which
    interactions with the underlying files and directories take place. This
    includes fetching the FilesysElement objects that represent the
    files/directories at certain paths.
    """
    element_types = Enum('FILE', 'DIR', 'LINK')

    @abc.abstractmethod
    def lookup_type(self, path):
        """
        Lookup the type of a given path.

        Args:
            @path
            The path to the file, dir, or link to lookup.
        """

    @abc.abstractmethod
    def chmod(self, path, mode, *args, **kwargs):  # pragma: no cover
        """
        Chmods the file or directory to a given mode, in the form of an octal
        int. Follows symlinks.

        On real files and directories, should behave like

        >>> os.chmod(path, mode)

        Args:
            @path
            The path to the file to chmod.

            @mode
            An octal int for ugo-style permissions, as in 0o777 or 0o644
        """

    @abc.abstractmethod
    def chown(self, path, uid, gid, *args, **kwargs):  # pragma: no cover
        """
        Chowns the file or directory to a given user in the form of a uid and a
        given group in the form of a gid. Does not follow symlinks.

        On real files and directories, should behave like

        >>> os.lchown(path, uid, gid)

        If one of the values is not changing, this can be handled through a
        stat() call, a la

        >>> self.chown(path, new_uid, self.stat(path).st_gid)

        or

        >>> self.chown(path, self.stat(path).st_uid, new_gid)

        Args:
            @path
            The path to the file, directory, or link to chown.

            @uid
            The integer uid to set on the file or directory.

            @gid
            The integer gid to set on the file or directory.
        """

    @abc.abstractmethod
    def stat(self, path, *args, **kwargs):  # pragma: no cover
        """
        Get the results of a stat on the file or directory, raising an OSError
        (errno=2, no such file or directory) if it does not exist. Only
        guaranteed to return an object, of an unspecified type, with the
        attributes
            st_mode - protection bits
            st_ino - inode number
            st_nlink - number of hard links
            st_uid - user id of owner
            st_gid - group id of owner
            st_size - size of file, in bytes
            st_atime - time of most recent access
            st_mtime - time of most recent content modification
            st_ctime - time of most recent metadata change

        Does not follow symlinks, so on real filesystem access, this is the
        equivalent of doing

        >>> os.lstat(self.path)

        but it is not safe to assume that nothing else happens. There may be
        other processing at play. In other words, don't call os.lstat on the
        underlying file and expect the same things to happen.

        Args:
            @path
            The path to the file, dir, or link to stat
        """

    @abc.abstractmethod
    def access(self, path, mode, *args, **kwargs):  # pragma: no cover
        """
        Preform an access check as with os.access, returning True or False
        depending on the type of access available.
        Follows symlinks, assuming (as on Linux platforms) that symlink
        permissions are those of the destination.

        Args:
            @path
            The path to the file whose access should be checked.

            @mode
            As per os.access, this must either be
                filesys.access_codes.F_OK
            or an inclusive OR of
                filesys.access_codes.W_OK
                filesys.access_codes.R_OK
                filesys.access_codes.X_OK
            When operating on a real file, arguments are presumably passed
            transparently through to the os.access() function, after a
            translation step.
        """

    @abc.abstractmethod
    def exists(self, path, *args, **kwargs):  # pragma: no cover
        """
        Returns true if the file/dir exists, and false if it does not.

        This is meaningful in general, as creating an abstract file or
        directory to refer to a location does not necessarily indicate that any
        file or directory creation has taken place. That would require an
        explicit creation action, which in the case of a virtualized filesys
        means (more or less) flipping a boolean flag, and in the case of a real
        filesystem means applying a mkdir or touch operation.

        Does not follow symlinks, so this is not equivalent to an invocation of

        >>> self.access(path, os.F_OK)

        but instead will behave much like

        >>> os.lexists(path)

        in which broken symlinks still appear as "present"
        """

    @abc.abstractmethod
    def hash(self, path, *args, **kwargs):  # pragma: no cover
        """
        Get a hash of the contents of the file or link at @path.
        Generally used for quick comparison of files and symlinks, much like a
        symbol table for fast string comparisons. This will be a sha512 hash of
        any files, and a sha256 hash of any symlinks' paths (not the contents
        of the linked file).
        The hash() operation has no significance on directories, and its
        behavior on them is undefined.

        Args:
            @path
            The path to the file or link to hash.
        """

    @abc.abstractmethod
    def copy(self, src, dst, *args, **kwargs):  # pragma: no cover
        """
        Copies a file or directory (recursive) from the source to the
        destination, given as paths.

        Args:
            @src
            The source file, as a path.

            @dst
            The destination file, as a path.
        """

    @abc.abstractmethod
    def walk(self, path, *args, **kwargs):  # pragma: no cover
        """
        An iterator over the contents of the directory at @path, yielding
        3-tuples of the form

        (dirname, subdirs, files)

        where dirname is the path to the directory rooted at @path, subdirs
        is a list of subdirectory names (which will be included in later
        iterations), and files is a list of file names found inside of the
        directory at dirname.

        If the directory does not exist, the iterator is empty, as opposed to a
        single iteration with (name, [], []), as would be given if it were an
        existing empty directory.

        Args:
            @path
            The path to the directory to walk.
        """

    @abc.abstractmethod
    def touch(self, path, *args, **kwargs):  # pragma: no cover
        """
        Create a file, using the default system umask (whatever it is) and
        the current euid and egid values. Doesn't do any fancy logic about
        chmoding or chowning the file after creation -- simply brings it into
        the world in whatever state is sensible.

        Args:
            @path
            The path to the file to create.
        """

    @abc.abstractmethod
    def mkdir(self, path, recursive=True, *args, **kwargs):  # pragma: no cover
        """
        Create the directory at @path. Behaves very much like touch(),
        using the current umask and not performing any special logic on the
        resulting directory.

        Args:
            @path
            The path to the directory to create.

        KWArgs:
            @recursive=True
            When true, create all parent directories necessary for a directory
            to exist. When false, if an ancestor directory does not exist, an
            OSError will be thrown.
        """

    @abc.abstractmethod
    def symlink(self, path, target, *args, **kwargs):  # pragma: no cover
        """
        Creates a symlink at @target to @path

        Like touch(), this does not do any special logic on
        ownership &c, but simply creates the symlink with whatever settings are
        applied by the current effective uid and gid.

        Args:
            @path
            The destination path to which the symlink points

            @target
            The target location at which the symlink should be created
        """

    @abc.abstractmethod
    @contextmanager
    def open(self, path, *args, **kwargs):  # pragma: no cover
        """
        Open the file at @path, returning a file-like object for interaction.
        Since this method itself is a contextmanager, it must follow the
        contextlib __enter__, yield, __exit__ paradigm. It can be used to wrap
        existing contextmanagers like so:

        >>> def open(self, mode):
        >>>     enter()
        >>>     with open(path, *args, **kwargs) as f:
        >>>         yield f
        >>>     exit()
        """

    def get_existing_ancestor(self, path):
        """
        Find the nearest ancestor of a file, dir, or link that passes an
        existence check.

        Args:
            @path
            An absolute path whose prefix should be inspected.
        """

    def writable_path_or_ancestor(self, path):
        """
        Check if a path points at a writable file/dir.
        Doesn't restrict itself to the results of access(), but also checks
        access on parent dirs,

        Args:
            @path
            An absolute path to a file/dir which should be inspected.
        """
        if self.access(path, access_codes.W_OK):
            return True
        if self.access(path, access_codes.F_OK):
            return False

        # at this point, the file/dir is known not to exist
        # now check properties of the containing dir
        containing_dir = self.get_existing_ancestor(path)
        if self.access(containing_dir, access_codes.W_OK):
            return True

        # if the file doesn't exist, and the dir containing it
        # isn't writable, then @path can't be written
        return False