imip-agent

Annotated imiptools/filesys.py

1382:b41360587104
2017-10-31 Paul Boddie Permit the generation of cancellation messages for all attendees. client-editing-simplification
paul@147 1
#!/usr/bin/env python
paul@147 2
paul@147 3
"""
paul@147 4
Filesystem utilities.
paul@147 5
paul@1210 6
Copyright (C) 2014, 2015, 2017 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@1210 23
from imiptools.config import settings
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@1210 28
DEFAULT_PERMISSIONS = settings["DEFAULT_PERMISSIONS"]
paul@1210 29
DEFAULT_DIR_PERMISSIONS = settings["DEFAULT_DIR_PERMISSIONS"]
paul@1210 30
paul@694 31
def check_dir(base, filename):
paul@694 32
paul@694 33
    "Return whether 'base' contains 'filename'."
paul@694 34
paul@694 35
    return commonprefix([base, abspath(filename)]) == base
paul@694 36
paul@694 37
def remaining_parts(base, filename):
paul@694 38
paul@694 39
    "Return the remaining parts from 'base' provided by 'filename'."
paul@694 40
paul@694 41
    if not check_dir(base, filename):
paul@694 42
        return None
paul@694 43
paul@694 44
    filename = abspath(filename)
paul@694 45
paul@694 46
    parts = []
paul@694 47
    while True:
paul@694 48
        filename, part = split(filename)
paul@694 49
        if check_dir(base, filename):
paul@694 50
            parts.insert(0, part)
paul@694 51
        else:
paul@694 52
            break
paul@694 53
paul@694 54
    return parts
paul@147 55
paul@330 56
def fix_permissions(filename, is_dir=False):
paul@694 57
paul@694 58
    """
paul@694 59
    Fix permissions for 'filename', with 'is_dir' indicating whether the object
paul@694 60
    should be a directory or not.
paul@694 61
    """
paul@694 62
paul@147 63
    try:
paul@330 64
        chmod(filename, is_dir and DEFAULT_DIR_PERMISSIONS or DEFAULT_PERMISSIONS)
paul@147 65
    except OSError:
paul@147 66
        pass
paul@147 67
paul@356 68
def make_path(base, parts):
paul@694 69
paul@694 70
    """
paul@694 71
    Make the path within 'base' having components defined by the given 'parts'.
paul@694 72
    Note that this function does not check the parts for suitability. To do so,
paul@694 73
    use the FileBase methods instead.
paul@694 74
    """
paul@694 75
paul@356 76
    for part in parts:
paul@356 77
        pathname = join(base, part)
paul@356 78
        if not exists(pathname):
paul@356 79
            mkdir(pathname)
paul@356 80
            fix_permissions(pathname, True)
paul@356 81
        base = pathname
paul@356 82
paul@147 83
class FileBase:
paul@147 84
paul@147 85
    "Basic filesystem operations."
paul@147 86
paul@303 87
    lock_name = "__lock__"
paul@303 88
paul@147 89
    def __init__(self, store_dir):
paul@953 90
        self.store_dir = abspath(store_dir)
paul@147 91
        if not exists(self.store_dir):
paul@356 92
            makedirs(self.store_dir)
paul@356 93
            fix_permissions(self.store_dir, True)
paul@703 94
        self.lock_depth = 0
paul@147 95
paul@147 96
    def get_file_object(self, base, *parts):
paul@694 97
paul@694 98
        """
paul@694 99
        Within the given 'base' location, return a path corresponding to the
paul@694 100
        given 'parts'.
paul@694 101
        """
paul@694 102
paul@694 103
        # Handle "empty" components.
paul@694 104
paul@147 105
        pathname = join(base, *parts)
paul@147 106
        return check_dir(base, pathname) and pathname or None
paul@147 107
paul@147 108
    def get_object_in_store(self, *parts):
paul@147 109
paul@147 110
        """
paul@147 111
        Return the name of any valid object stored within a hierarchy specified
paul@147 112
        by the given 'parts'.
paul@147 113
        """
paul@147 114
paul@147 115
        parent = expected = self.store_dir
paul@147 116
paul@694 117
        # Handle "empty" components.
paul@694 118
paul@694 119
        parts = [p for p in parts if p]
paul@694 120
paul@147 121
        for part in parts:
paul@147 122
            filename = self.get_file_object(expected, part)
paul@147 123
            if not filename:
paul@953 124
                return None
paul@147 125
            parent = expected
paul@147 126
            expected = filename
paul@147 127
paul@147 128
        if not exists(parent):
paul@356 129
            make_path(self.store_dir, parts[:-1])
paul@147 130
paul@147 131
        return filename
paul@147 132
paul@694 133
    def move_object(self, source, target):
paul@694 134
paul@694 135
        "Move 'source' to 'target'."
paul@694 136
paul@694 137
        if not self.ensure_parent(target):
paul@694 138
            return False
paul@694 139
        rename(source, target)
paul@694 140
paul@694 141
    def ensure_parent(self, target):
paul@694 142
paul@694 143
        "Ensure that the parent of 'target' exists."
paul@694 144
paul@694 145
        parts = remaining_parts(self.store_dir, target)
paul@694 146
        if not parts or not self.get_file_object(self.store_dir, *parts[:-1]):
paul@694 147
            return False
paul@694 148
paul@694 149
        make_path(self.store_dir, parts[:-1])
paul@694 150
        return True
paul@694 151
paul@303 152
    # Locking methods.
paul@303 153
    # This uses the directory creation method exploited by MoinMoin.util.lock.
paul@303 154
    # However, a simple single lock type mechanism is employed here.
paul@303 155
paul@702 156
    def make_lock_dir(self, *parts):
paul@694 157
paul@702 158
        "Make the lock directory defined by the given 'parts'."
paul@694 159
paul@303 160
        parts = parts and list(parts) or []
paul@303 161
        parts.append(self.lock_name)
paul@953 162
        d = self.get_object_in_store(*parts)
paul@953 163
        if not d: raise OSError(errno.ENOENT, "Could not get lock in store: %r in %r" % (parts, self.store_dir))
paul@953 164
        mkdir(d)
paul@702 165
        parts.append(str(getpid()))
paul@953 166
        d = self.get_object_in_store(*parts)
paul@953 167
        if not d: raise OSError(errno.ENOENT, "Could not get lock in store: %r in %r" % (parts, self.store_dir))
paul@953 168
        mkdir(d)
paul@702 169
paul@702 170
    def remove_lock_dir(self, *parts):
paul@702 171
paul@702 172
        "Remove the lock directory defined by the given 'parts'."
paul@702 173
paul@702 174
        parts = parts and list(parts) or []
paul@715 175
paul@702 176
        parts.append(self.lock_name)
paul@1021 177
        parts.append(str(getpid()))
paul@1021 178
        rmdir(self.get_object_in_store(*parts))
paul@1021 179
        parts.pop()
paul@1021 180
        rmdir(self.get_object_in_store(*parts))
paul@702 181
paul@702 182
    def owning_lock_dir(self, *parts):
paul@702 183
paul@702 184
        "Return whether this process owns the lock directory."
paul@702 185
paul@702 186
        parts = parts and list(parts) or []
paul@702 187
        parts.append(self.lock_name)
paul@702 188
        parts.append(str(getpid()))
paul@702 189
        return exists(self.get_object_in_store(*parts))
paul@303 190
paul@303 191
    def acquire_lock(self, timeout=None, *parts):
paul@303 192
paul@303 193
        """
paul@303 194
        Acquire an exclusive lock on the directory or a path within it described
paul@303 195
        by 'parts'.
paul@303 196
        """
paul@303 197
paul@303 198
        start = now = time()
paul@303 199
paul@303 200
        while not timeout or now - start < timeout:
paul@303 201
            try:
paul@702 202
                self.make_lock_dir(*parts)
paul@303 203
                break
paul@303 204
            except OSError, exc:
paul@303 205
                if exc.errno != errno.EEXIST:
paul@303 206
                    raise
paul@702 207
                elif self.owning_lock_dir(*parts):
paul@703 208
                    self.lock_depth += 1
paul@702 209
                    break
paul@303 210
            sleep(1)
paul@303 211
            now = time()
paul@303 212
paul@303 213
    def release_lock(self, *parts):
paul@303 214
paul@303 215
        """
paul@303 216
        Release an acquired lock on the directory or a path within it described
paul@303 217
        by 'parts'.
paul@303 218
        """
paul@303 219
paul@303 220
        try:
paul@703 221
            if self.lock_depth != 0:
paul@703 222
                self.lock_depth -= 1
paul@703 223
            else:
paul@703 224
                self.remove_lock_dir(*parts)
paul@303 225
        except OSError, exc:
paul@714 226
            if exc.errno not in (errno.ENOENT, errno.ENOTEMPTY):
paul@303 227
                raise
paul@303 228
paul@147 229
# vim: tabstop=4 expandtab shiftwidth=4