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