1 #!/usr/bin/env python 2 3 """ 4 Directory repositories for WebStack. 5 6 Copyright (C) 2005 Paul Boddie <paul@boddie.org.uk> 7 8 This library is free software; you can redistribute it and/or 9 modify it under the terms of the GNU Lesser General Public 10 License as published by the Free Software Foundation; either 11 version 2.1 of the License, or (at your option) any later version. 12 13 This library is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 Lesser General Public License for more details. 17 18 You should have received a copy of the GNU Lesser General Public 19 License along with this library; if not, write to the Free Software 20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 21 """ 22 23 import os 24 25 class DirectoryRepository: 26 27 "A directory repository providing session-like access to files." 28 29 new_filename = "__new__" 30 31 def __init__(self, path, fsencoding=None, delay=1): 32 33 """ 34 Initialise the repository using the given 'path' to indicate the 35 location of the repository. If no such location exists in the filesystem 36 an attempt will be made to create the directory. 37 38 The optional 'fsencoding' parameter can be used to assert a particular 39 character encoding used by the filesystem to represent filenames. By 40 default, the default encoding is detected (or Unicode objects are used 41 if appropriate). 42 43 The optional 'delay' argument specifies the time in seconds between each 44 poll of an opened repository file when that file is found to be locked 45 for editing. 46 """ 47 48 # Convert the path to an absolute path. 49 50 self.path = os.path.abspath(path) 51 self.fsencoding = fsencoding 52 self.delay = delay 53 54 # Create a directory and initialise it with a special file. 55 56 if not os.path.exists(path): 57 os.mkdir(path) 58 f = open(self.full_path(self.new_filename), "wb") 59 f.close() 60 61 # Guess the filesystem encoding. 62 63 if fsencoding is None: 64 if os.path.supports_unicode_filenames: 65 self.fsencoding = None 66 else: 67 import locale 68 self.fsencoding = locale.getdefaultlocale()[1] 69 70 # Or override any guesses. 71 72 else: 73 self.fsencoding = fsencoding 74 75 def _convert_name(self, name): 76 77 "Convert the given 'name' to a plain string in the filesystem encoding." 78 79 if self.fsencoding: 80 return name.encode(self.fsencoding) 81 else: 82 return name 83 84 def _convert_fsname(self, name): 85 86 """ 87 Convert the given 'name' as a plain string in the filesystem encoding to 88 a Unicode object. 89 """ 90 91 if self.fsencoding: 92 return unicode(name, self.fsencoding) 93 else: 94 return name 95 96 def keys(self): 97 98 "Return the names of the files stored in the repository." 99 100 # NOTE: Special names converted using a simple rule. 101 l = [] 102 for name in os.listdir(self.path): 103 if name.endswith(".edit"): 104 l.append(name[:-5]) 105 elif name == self.new_filename: 106 pass 107 else: 108 l.append(name) 109 return map(self._convert_fsname, l) 110 111 def full_path(self, key, edit=0): 112 113 """ 114 Return the full path to the 'key' in the filesystem. If the optional 115 'edit' parameter is set to a true value, the returned path will refer to 116 the editable version of the file. 117 """ 118 119 # NOTE: Special names converted using a simple rule. 120 path = os.path.abspath(os.path.join(self.path, self._convert_name(key))) 121 if edit: 122 path = path + ".edit" 123 if not path.startswith(self.path): 124 raise ValueError, key 125 else: 126 return path 127 128 def edit_path(self, key): 129 130 """ 131 Return the full path to the 'key' in the filesystem provided that the 132 file associated with the 'key' is locked for editing. 133 """ 134 135 return self.full_path(key, edit=1) 136 137 def has_key(self, key): 138 139 """ 140 Return whether a file with the name specified by 'key is stored in the 141 repository. 142 """ 143 144 return key in self.keys() 145 146 # NOTE: Methods very similar to Helpers.Session.Wrapper. 147 148 def items(self): 149 150 """ 151 Return a list of (name, value) tuples for the files stored in the 152 repository. 153 """ 154 155 results = [] 156 for key in self.keys(): 157 results.append((key, self[key])) 158 return results 159 160 def values(self): 161 162 "Return the contents of the files stored in the repository." 163 164 results = [] 165 for key in self.keys(): 166 results.append(self[key]) 167 return results 168 169 def lock(self, key, create=0, opener=None): 170 171 """ 172 Lock the file associated with the given 'key'. If the optional 'create' 173 parameter is set to a true value (unlike the default), the file will be 174 created if it did not already exist; otherwise, a KeyError will be 175 raised. 176 177 If the optional 'opener' parameter is specified, it will be used to 178 create any new file in the case where 'create' is set to a true value; 179 otherwise, the standard 'open' function will be used to create the file. 180 181 Return the full path to the editable file. 182 """ 183 184 path = self.full_path(key) 185 edit_path = self.edit_path(key) 186 187 # Attempt to lock the file by renaming it. 188 # NOTE: This assumes that renaming is an atomic operation. 189 190 if os.path.exists(path) or os.path.exists(edit_path): 191 while 1: 192 try: 193 os.rename(path, edit_path) 194 except OSError: 195 time.sleep(self.delay) 196 else: 197 break 198 199 # Where a file does not exist, attempt to create a new file. 200 # Since file creation is probably not atomic, we use the renaming of a 201 # special file in an attempt to impose some kind of atomic "bottleneck". 202 203 elif create: 204 205 # NOTE: Avoid failure case where no __new__ file exists for some 206 # NOTE: reason. 207 208 try: 209 self.lock(self.new_filename) 210 except KeyError: 211 f = open(self.edit_path(self.new_filename), "wb") 212 f.close() 213 214 try: 215 if opener is None: 216 f = open(edit_path, "wb") 217 f.close() 218 else: 219 f = opener(edit_path) 220 f.close() 221 finally: 222 self.unlock(self.new_filename) 223 224 # Where no creation is requested, raise an exception. 225 226 else: 227 raise KeyError, key 228 229 return edit_path 230 231 def unlock(self, key): 232 233 """ 234 Unlock the file associated with the given 'key'. 235 236 Important note: this method should be used in a finally clause in order 237 to avoid files being locked and never being unlocked by the same process 238 because an unhandled exception was raised. 239 """ 240 241 path = self.full_path(key) 242 edit_path = self.edit_path(key) 243 os.rename(edit_path, path) 244 245 def __delitem__(self, key): 246 247 "Remove the file associated with the given 'key'." 248 249 path = self.full_path(key) 250 edit_path = self.edit_path(key) 251 if os.path.exists(path) or os.path.exists(edit_path): 252 while 1: 253 try: 254 os.remove(path) 255 except OSError: 256 time.sleep(self.delay) 257 else: 258 break 259 else: 260 raise KeyError, key 261 262 def __getitem__(self, key): 263 264 "Return the contents of the file associated with the given 'key'." 265 266 edit_path = self.lock(key, create=0) 267 try: 268 f = open(edit_path, "rb") 269 s = "" 270 try: 271 s = f.read() 272 finally: 273 f.close() 274 finally: 275 self.unlock(key) 276 277 return s 278 279 def __setitem__(self, key, value): 280 281 """ 282 Set the contents of the file associated with the given 'key' using the 283 given 'value'. 284 """ 285 286 edit_path = self.lock(key, create=1) 287 try: 288 f = open(edit_path, "wb") 289 try: 290 f.write(value) 291 finally: 292 f.close() 293 finally: 294 self.unlock(key) 295 296 # vim: tabstop=4 expandtab shiftwidth=4