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