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