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. 176 177 parts.append(self.lock_name) 178 target.append(self.old_lock_name) 179 rename(self.get_object_in_store(*parts), self.get_object_in_store(*target)) 180 181 # Then remove the moved directory and its contents. 182 183 target.append(str(getpid())) 184 rmdir(self.get_object_in_store(*target)) 185 target.pop() 186 rmdir(self.get_object_in_store(*target)) 187 188 def owning_lock_dir(self, *parts): 189 190 "Return whether this process owns the lock directory." 191 192 parts = parts and list(parts) or [] 193 parts.append(self.lock_name) 194 parts.append(str(getpid())) 195 return exists(self.get_object_in_store(*parts)) 196 197 def acquire_lock(self, timeout=None, *parts): 198 199 """ 200 Acquire an exclusive lock on the directory or a path within it described 201 by 'parts'. 202 """ 203 204 start = now = time() 205 206 while not timeout or now - start < timeout: 207 try: 208 self.make_lock_dir(*parts) 209 break 210 except OSError, exc: 211 if exc.errno != errno.EEXIST: 212 raise 213 elif self.owning_lock_dir(*parts): 214 self.lock_depth += 1 215 break 216 sleep(1) 217 now = time() 218 219 def release_lock(self, *parts): 220 221 """ 222 Release an acquired lock on the directory or a path within it described 223 by 'parts'. 224 """ 225 226 try: 227 if self.lock_depth != 0: 228 self.lock_depth -= 1 229 else: 230 self.remove_lock_dir(*parts) 231 except OSError, exc: 232 if exc.errno not in (errno.ENOENT, errno.ENOTEMPTY): 233 raise 234 235 # vim: tabstop=4 expandtab shiftwidth=4