imip-agent

Annotated imiptools/filesys.py

787:9cf10fe21c3a
2015-09-28 Paul Boddie Separated attendee/recurrence manipulation from presentation, introducing form field dictionary updates from form period/date objects, also simplifying the processing of attendees, removing filtering operations during editing. imipweb-client-simplification
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@715 85
    old_lock_name = "__unlock__"
paul@303 86
paul@147 87
    def __init__(self, store_dir):
paul@147 88
        self.store_dir = store_dir
paul@147 89
        if not exists(self.store_dir):
paul@356 90
            makedirs(self.store_dir)
paul@356 91
            fix_permissions(self.store_dir, True)
paul@703 92
        self.lock_depth = 0
paul@147 93
paul@147 94
    def get_file_object(self, base, *parts):
paul@694 95
paul@694 96
        """
paul@694 97
        Within the given 'base' location, return a path corresponding to the
paul@694 98
        given 'parts'.
paul@694 99
        """
paul@694 100
paul@694 101
        # Handle "empty" components.
paul@694 102
paul@147 103
        pathname = join(base, *parts)
paul@147 104
        return check_dir(base, pathname) and pathname or None
paul@147 105
paul@147 106
    def get_object_in_store(self, *parts):
paul@147 107
paul@147 108
        """
paul@147 109
        Return the name of any valid object stored within a hierarchy specified
paul@147 110
        by the given 'parts'.
paul@147 111
        """
paul@147 112
paul@147 113
        parent = expected = self.store_dir
paul@147 114
paul@694 115
        # Handle "empty" components.
paul@694 116
paul@694 117
        parts = [p for p in parts if p]
paul@694 118
paul@147 119
        for part in parts:
paul@147 120
            filename = self.get_file_object(expected, part)
paul@147 121
            if not filename:
paul@147 122
                return False
paul@147 123
            parent = expected
paul@147 124
            expected = filename
paul@147 125
paul@147 126
        if not exists(parent):
paul@356 127
            make_path(self.store_dir, parts[:-1])
paul@147 128
paul@147 129
        return filename
paul@147 130
paul@694 131
    def move_object(self, source, target):
paul@694 132
paul@694 133
        "Move 'source' to 'target'."
paul@694 134
paul@694 135
        if not self.ensure_parent(target):
paul@694 136
            return False
paul@694 137
        rename(source, target)
paul@694 138
paul@694 139
    def ensure_parent(self, target):
paul@694 140
paul@694 141
        "Ensure that the parent of 'target' exists."
paul@694 142
paul@694 143
        parts = remaining_parts(self.store_dir, target)
paul@694 144
        if not parts or not self.get_file_object(self.store_dir, *parts[:-1]):
paul@694 145
            return False
paul@694 146
paul@694 147
        make_path(self.store_dir, parts[:-1])
paul@694 148
        return True
paul@694 149
paul@303 150
    # Locking methods.
paul@303 151
    # This uses the directory creation method exploited by MoinMoin.util.lock.
paul@303 152
    # However, a simple single lock type mechanism is employed here.
paul@303 153
paul@702 154
    def make_lock_dir(self, *parts):
paul@694 155
paul@702 156
        "Make the lock directory defined by the given 'parts'."
paul@694 157
paul@303 158
        parts = parts and list(parts) or []
paul@303 159
        parts.append(self.lock_name)
paul@715 160
        mkdir(self.get_object_in_store(*parts))
paul@702 161
        parts.append(str(getpid()))
paul@715 162
        mkdir(self.get_object_in_store(*parts))
paul@702 163
paul@702 164
    def remove_lock_dir(self, *parts):
paul@702 165
paul@702 166
        "Remove the lock directory defined by the given 'parts'."
paul@702 167
paul@702 168
        parts = parts and list(parts) or []
paul@715 169
        target = parts[:]
paul@715 170
paul@715 171
        # Move the directory.
paul@715 172
paul@702 173
        parts.append(self.lock_name)
paul@715 174
        target.append(self.old_lock_name)
paul@715 175
        rename(self.get_object_in_store(*parts), self.get_object_in_store(*target))
paul@715 176
paul@715 177
        # Then remove the moved directory and its contents.
paul@715 178
paul@715 179
        target.append(str(getpid()))
paul@715 180
        rmdir(self.get_object_in_store(*target))
paul@715 181
        target.pop()
paul@715 182
        rmdir(self.get_object_in_store(*target))
paul@702 183
paul@702 184
    def owning_lock_dir(self, *parts):
paul@702 185
paul@702 186
        "Return whether this process owns the lock directory."
paul@702 187
paul@702 188
        parts = parts and list(parts) or []
paul@702 189
        parts.append(self.lock_name)
paul@702 190
        parts.append(str(getpid()))
paul@702 191
        return exists(self.get_object_in_store(*parts))
paul@303 192
paul@303 193
    def acquire_lock(self, timeout=None, *parts):
paul@303 194
paul@303 195
        """
paul@303 196
        Acquire an exclusive lock on the directory or a path within it described
paul@303 197
        by 'parts'.
paul@303 198
        """
paul@303 199
paul@303 200
        start = now = time()
paul@303 201
paul@303 202
        while not timeout or now - start < timeout:
paul@303 203
            try:
paul@702 204
                self.make_lock_dir(*parts)
paul@303 205
                break
paul@303 206
            except OSError, exc:
paul@303 207
                if exc.errno != errno.EEXIST:
paul@303 208
                    raise
paul@702 209
                elif self.owning_lock_dir(*parts):
paul@703 210
                    self.lock_depth += 1
paul@702 211
                    break
paul@303 212
            sleep(1)
paul@303 213
            now = time()
paul@303 214
paul@303 215
    def release_lock(self, *parts):
paul@303 216
paul@303 217
        """
paul@303 218
        Release an acquired lock on the directory or a path within it described
paul@303 219
        by 'parts'.
paul@303 220
        """
paul@303 221
paul@303 222
        try:
paul@703 223
            if self.lock_depth != 0:
paul@703 224
                self.lock_depth -= 1
paul@703 225
            else:
paul@703 226
                self.remove_lock_dir(*parts)
paul@303 227
        except OSError, exc:
paul@714 228
            if exc.errno not in (errno.ENOENT, errno.ENOTEMPTY):
paul@303 229
                raise
paul@303 230
paul@147 231
# vim: tabstop=4 expandtab shiftwidth=4