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