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