imip-agent

Annotated imiptools/filesys.py

1065:2175787d1896
2016-03-04 Paul Boddie Merged changes from the default branch. freebusy-collections
paul@147 1
#!/usr/bin/env python
paul@147 2
paul@147 3
"""
paul@147 4
Filesystem utilities.
paul@147 5
paul@147 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@147 7
paul@147 8
This program is free software; you can redistribute it and/or modify it under
paul@147 9
the terms of the GNU General Public License as published by the Free Software
paul@147 10
Foundation; either version 3 of the License, or (at your option) any later
paul@147 11
version.
paul@147 12
paul@147 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@147 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@147 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@147 16
details.
paul@147 17
paul@147 18
You should have received a copy of the GNU General Public License along with
paul@147 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@147 20
"""
paul@147 21
paul@303 22
import errno
paul@330 23
from imiptools.config import DEFAULT_PERMISSIONS, DEFAULT_DIR_PERMISSIONS
paul@694 24
from os.path import abspath, commonprefix, exists, join, split
paul@702 25
from os import chmod, getpid, makedirs, mkdir, rename, rmdir
paul@303 26
from time import sleep, time
paul@147 27
paul@694 28
def check_dir(base, filename):
paul@694 29
paul@694 30
    "Return whether 'base' contains 'filename'."
paul@694 31
paul@694 32
    return commonprefix([base, abspath(filename)]) == base
paul@694 33
paul@694 34
def remaining_parts(base, filename):
paul@694 35
paul@694 36
    "Return the remaining parts from 'base' provided by 'filename'."
paul@694 37
paul@694 38
    if not check_dir(base, filename):
paul@694 39
        return None
paul@694 40
paul@694 41
    filename = abspath(filename)
paul@694 42
paul@694 43
    parts = []
paul@694 44
    while True:
paul@694 45
        filename, part = split(filename)
paul@694 46
        if check_dir(base, filename):
paul@694 47
            parts.insert(0, part)
paul@694 48
        else:
paul@694 49
            break
paul@694 50
paul@694 51
    return parts
paul@147 52
paul@330 53
def fix_permissions(filename, is_dir=False):
paul@694 54
paul@694 55
    """
paul@694 56
    Fix permissions for 'filename', with 'is_dir' indicating whether the object
paul@694 57
    should be a directory or not.
paul@694 58
    """
paul@694 59
paul@147 60
    try:
paul@330 61
        chmod(filename, is_dir and DEFAULT_DIR_PERMISSIONS or DEFAULT_PERMISSIONS)
paul@147 62
    except OSError:
paul@147 63
        pass
paul@147 64
paul@356 65
def make_path(base, parts):
paul@694 66
paul@694 67
    """
paul@694 68
    Make the path within 'base' having components defined by the given 'parts'.
paul@694 69
    Note that this function does not check the parts for suitability. To do so,
paul@694 70
    use the FileBase methods instead.
paul@694 71
    """
paul@694 72
paul@356 73
    for part in parts:
paul@356 74
        pathname = join(base, part)
paul@356 75
        if not exists(pathname):
paul@356 76
            mkdir(pathname)
paul@356 77
            fix_permissions(pathname, True)
paul@356 78
        base = pathname
paul@356 79
paul@147 80
class FileBase:
paul@147 81
paul@147 82
    "Basic filesystem operations."
paul@147 83
paul@303 84
    lock_name = "__lock__"
paul@303 85
paul@147 86
    def __init__(self, store_dir):
paul@953 87
        self.store_dir = abspath(store_dir)
paul@147 88
        if not exists(self.store_dir):
paul@356 89
            makedirs(self.store_dir)
paul@356 90
            fix_permissions(self.store_dir, True)
paul@703 91
        self.lock_depth = 0
paul@147 92
paul@147 93
    def get_file_object(self, base, *parts):
paul@694 94
paul@694 95
        """
paul@694 96
        Within the given 'base' location, return a path corresponding to the
paul@694 97
        given 'parts'.
paul@694 98
        """
paul@694 99
paul@694 100
        # Handle "empty" components.
paul@694 101
paul@147 102
        pathname = join(base, *parts)
paul@147 103
        return check_dir(base, pathname) and pathname or None
paul@147 104
paul@147 105
    def get_object_in_store(self, *parts):
paul@147 106
paul@147 107
        """
paul@147 108
        Return the name of any valid object stored within a hierarchy specified
paul@147 109
        by the given 'parts'.
paul@147 110
        """
paul@147 111
paul@147 112
        parent = expected = self.store_dir
paul@147 113
paul@694 114
        # Handle "empty" components.
paul@694 115
paul@694 116
        parts = [p for p in parts if p]
paul@694 117
paul@147 118
        for part in parts:
paul@147 119
            filename = self.get_file_object(expected, part)
paul@147 120
            if not filename:
paul@953 121
                return None
paul@147 122
            parent = expected
paul@147 123
            expected = filename
paul@147 124
paul@147 125
        if not exists(parent):
paul@356 126
            make_path(self.store_dir, parts[:-1])
paul@147 127
paul@147 128
        return filename
paul@147 129
paul@694 130
    def move_object(self, source, target):
paul@694 131
paul@694 132
        "Move 'source' to 'target'."
paul@694 133
paul@694 134
        if not self.ensure_parent(target):
paul@694 135
            return False
paul@694 136
        rename(source, target)
paul@694 137
paul@694 138
    def ensure_parent(self, target):
paul@694 139
paul@694 140
        "Ensure that the parent of 'target' exists."
paul@694 141
paul@694 142
        parts = remaining_parts(self.store_dir, target)
paul@694 143
        if not parts or not self.get_file_object(self.store_dir, *parts[:-1]):
paul@694 144
            return False
paul@694 145
paul@694 146
        make_path(self.store_dir, parts[:-1])
paul@694 147
        return True
paul@694 148
paul@303 149
    # Locking methods.
paul@303 150
    # This uses the directory creation method exploited by MoinMoin.util.lock.
paul@303 151
    # However, a simple single lock type mechanism is employed here.
paul@303 152
paul@702 153
    def make_lock_dir(self, *parts):
paul@694 154
paul@702 155
        "Make the lock directory defined by the given 'parts'."
paul@694 156
paul@303 157
        parts = parts and list(parts) or []
paul@303 158
        parts.append(self.lock_name)
paul@953 159
        d = self.get_object_in_store(*parts)
paul@953 160
        if not d: raise OSError(errno.ENOENT, "Could not get lock in store: %r in %r" % (parts, self.store_dir))
paul@953 161
        mkdir(d)
paul@702 162
        parts.append(str(getpid()))
paul@953 163
        d = self.get_object_in_store(*parts)
paul@953 164
        if not d: raise OSError(errno.ENOENT, "Could not get lock in store: %r in %r" % (parts, self.store_dir))
paul@953 165
        mkdir(d)
paul@702 166
paul@702 167
    def remove_lock_dir(self, *parts):
paul@702 168
paul@702 169
        "Remove the lock directory defined by the given 'parts'."
paul@702 170
paul@702 171
        parts = parts and list(parts) or []
paul@715 172
paul@702 173
        parts.append(self.lock_name)
paul@1021 174
        parts.append(str(getpid()))
paul@1021 175
        rmdir(self.get_object_in_store(*parts))
paul@1021 176
        parts.pop()
paul@1021 177
        rmdir(self.get_object_in_store(*parts))
paul@702 178
paul@702 179
    def owning_lock_dir(self, *parts):
paul@702 180
paul@702 181
        "Return whether this process owns the lock directory."
paul@702 182
paul@702 183
        parts = parts and list(parts) or []
paul@702 184
        parts.append(self.lock_name)
paul@702 185
        parts.append(str(getpid()))
paul@702 186
        return exists(self.get_object_in_store(*parts))
paul@303 187
paul@303 188
    def acquire_lock(self, timeout=None, *parts):
paul@303 189
paul@303 190
        """
paul@303 191
        Acquire an exclusive lock on the directory or a path within it described
paul@303 192
        by 'parts'.
paul@303 193
        """
paul@303 194
paul@303 195
        start = now = time()
paul@303 196
paul@303 197
        while not timeout or now - start < timeout:
paul@303 198
            try:
paul@702 199
                self.make_lock_dir(*parts)
paul@303 200
                break
paul@303 201
            except OSError, exc:
paul@303 202
                if exc.errno != errno.EEXIST:
paul@303 203
                    raise
paul@702 204
                elif self.owning_lock_dir(*parts):
paul@703 205
                    self.lock_depth += 1
paul@702 206
                    break
paul@303 207
            sleep(1)
paul@303 208
            now = time()
paul@303 209
paul@303 210
    def release_lock(self, *parts):
paul@303 211
paul@303 212
        """
paul@303 213
        Release an acquired lock on the directory or a path within it described
paul@303 214
        by 'parts'.
paul@303 215
        """
paul@303 216
paul@303 217
        try:
paul@703 218
            if self.lock_depth != 0:
paul@703 219
                self.lock_depth -= 1
paul@703 220
            else:
paul@703 221
                self.remove_lock_dir(*parts)
paul@303 222
        except OSError, exc:
paul@714 223
            if exc.errno not in (errno.ENOENT, errno.ENOTEMPTY):
paul@303 224
                raise
paul@303 225
paul@147 226
# vim: tabstop=4 expandtab shiftwidth=4