1 #!/usr/bin/env python 2 3 """ 4 Filesystem utilities. 5 6 Copyright (C) 2014, 2015, 2017 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 settings 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 DEFAULT_PERMISSIONS = settings["DEFAULT_PERMISSIONS"] 29 DEFAULT_DIR_PERMISSIONS = settings["DEFAULT_DIR_PERMISSIONS"] 30 31 def check_dir(base, filename): 32 33 "Return whether 'base' contains 'filename'." 34 35 return commonprefix([base, abspath(filename)]) == base 36 37 def remaining_parts(base, filename): 38 39 "Return the remaining parts from 'base' provided by 'filename'." 40 41 if not check_dir(base, filename): 42 return None 43 44 filename = abspath(filename) 45 46 parts = [] 47 while True: 48 filename, part = split(filename) 49 if check_dir(base, filename): 50 parts.insert(0, part) 51 else: 52 break 53 54 return parts 55 56 def fix_permissions(filename, is_dir=False): 57 58 """ 59 Fix permissions for 'filename', with 'is_dir' indicating whether the object 60 should be a directory or not. 61 """ 62 63 try: 64 chmod(filename, is_dir and DEFAULT_DIR_PERMISSIONS or DEFAULT_PERMISSIONS) 65 except OSError: 66 pass 67 68 def make_path(base, parts): 69 70 """ 71 Make the path within 'base' having components defined by the given 'parts'. 72 Note that this function does not check the parts for suitability. To do so, 73 use the FileBase methods instead. 74 """ 75 76 for part in parts: 77 pathname = join(base, part) 78 if not exists(pathname): 79 mkdir(pathname) 80 fix_permissions(pathname, True) 81 base = pathname 82 83 class FileBase: 84 85 "Basic filesystem operations." 86 87 lock_name = "__lock__" 88 89 def __init__(self, store_dir): 90 self.store_dir = abspath(store_dir) 91 if not exists(self.store_dir): 92 makedirs(self.store_dir) 93 fix_permissions(self.store_dir, True) 94 self.lock_depth = 0 95 96 def get_file_object(self, base, *parts): 97 98 """ 99 Within the given 'base' location, return a path corresponding to the 100 given 'parts'. 101 """ 102 103 # Handle "empty" components. 104 105 pathname = join(base, *parts) 106 return check_dir(base, pathname) and pathname or None 107 108 def get_object_in_store(self, *parts): 109 110 """ 111 Return the name of any valid object stored within a hierarchy specified 112 by the given 'parts'. 113 """ 114 115 parent = expected = self.store_dir 116 117 # Handle "empty" components. 118 119 parts = [p for p in parts if p] 120 121 for part in parts: 122 filename = self.get_file_object(expected, part) 123 if not filename: 124 return None 125 parent = expected 126 expected = filename 127 128 if not exists(parent): 129 make_path(self.store_dir, parts[:-1]) 130 131 return filename 132 133 def move_object(self, source, target): 134 135 "Move 'source' to 'target'." 136 137 if not self.ensure_parent(target): 138 return False 139 rename(source, target) 140 141 def ensure_parent(self, target): 142 143 "Ensure that the parent of 'target' exists." 144 145 parts = remaining_parts(self.store_dir, target) 146 if not parts or not self.get_file_object(self.store_dir, *parts[:-1]): 147 return False 148 149 make_path(self.store_dir, parts[:-1]) 150 return True 151 152 # Locking methods. 153 # This uses the directory creation method exploited by MoinMoin.util.lock. 154 # However, a simple single lock type mechanism is employed here. 155 156 def make_lock_dir(self, *parts): 157 158 "Make the lock directory defined by the given 'parts'." 159 160 parts = parts and list(parts) or [] 161 parts.append(self.lock_name) 162 d = self.get_object_in_store(*parts) 163 if not d: raise OSError(errno.ENOENT, "Could not get lock in store: %r in %r" % (parts, self.store_dir)) 164 mkdir(d) 165 parts.append(str(getpid())) 166 d = self.get_object_in_store(*parts) 167 if not d: raise OSError(errno.ENOENT, "Could not get lock in store: %r in %r" % (parts, self.store_dir)) 168 mkdir(d) 169 170 def remove_lock_dir(self, *parts): 171 172 "Remove the lock directory defined by the given 'parts'." 173 174 parts = parts and list(parts) or [] 175 176 parts.append(self.lock_name) 177 parts.append(str(getpid())) 178 rmdir(self.get_object_in_store(*parts)) 179 parts.pop() 180 rmdir(self.get_object_in_store(*parts)) 181 182 def owning_lock_dir(self, *parts): 183 184 "Return whether this process owns the lock directory." 185 186 parts = parts and list(parts) or [] 187 parts.append(self.lock_name) 188 parts.append(str(getpid())) 189 return exists(self.get_object_in_store(*parts)) 190 191 def acquire_lock(self, timeout=None, *parts): 192 193 """ 194 Acquire an exclusive lock on the directory or a path within it described 195 by 'parts'. 196 """ 197 198 start = now = time() 199 200 while not timeout or now - start < timeout: 201 try: 202 self.make_lock_dir(*parts) 203 break 204 except OSError, exc: 205 if exc.errno != errno.EEXIST: 206 raise 207 elif self.owning_lock_dir(*parts): 208 self.lock_depth += 1 209 break 210 sleep(1) 211 now = time() 212 213 def release_lock(self, *parts): 214 215 """ 216 Release an acquired lock on the directory or a path within it described 217 by 'parts'. 218 """ 219 220 try: 221 if self.lock_depth != 0: 222 self.lock_depth -= 1 223 else: 224 self.remove_lock_dir(*parts) 225 except OSError, exc: 226 if exc.errno not in (errno.ENOENT, errno.ENOTEMPTY): 227 raise 228 229 # vim: tabstop=4 expandtab shiftwidth=4