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 name = name[:-5] 105 if name != self.new_filename: 106 l.append(name) 107 return map(self._convert_fsname, l) 108 109 def full_path(self, key, edit=0): 110 111 """ 112 Return the full path to the 'key' in the filesystem. If the optional 113 'edit' parameter is set to a true value, the returned path will refer to 114 the editable version of the file. 115 """ 116 117 # NOTE: Special names converted using a simple rule. 118 path = os.path.abspath(os.path.join(self.path, self._convert_name(key))) 119 if edit: 120 path = path + ".edit" 121 if not path.startswith(self.path): 122 raise ValueError, key 123 else: 124 return path 125 126 def edit_path(self, key): 127 128 """ 129 Return the full path to the 'key' in the filesystem provided that the 130 file associated with the 'key' is locked for editing. 131 """ 132 133 return self.full_path(key, edit=1) 134 135 def has_key(self, key): 136 137 """ 138 Return whether a file with the name specified by 'key is stored in the 139 repository. 140 """ 141 142 return key in self.keys() 143 144 # NOTE: Methods very similar to Helpers.Session.Wrapper. 145 146 def items(self): 147 148 """ 149 Return a list of (name, value) tuples for the files stored in the 150 repository. 151 """ 152 153 results = [] 154 for key in self.keys(): 155 results.append((key, self[key])) 156 return results 157 158 def values(self): 159 160 "Return the contents of the files stored in the repository." 161 162 results = [] 163 for key in self.keys(): 164 results.append(self[key]) 165 return results 166 167 def lock(self, key, create=0, opener=None): 168 169 """ 170 Lock the file associated with the given 'key'. If the optional 'create' 171 parameter is set to a true value (unlike the default), the file will be 172 created if it did not already exist; otherwise, a KeyError will be 173 raised. 174 175 If the optional 'opener' parameter is specified, it will be used to 176 create any new file in the case where 'create' is set to a true value; 177 otherwise, the standard 'open' function will be used to create the file. 178 179 Return the full path to the editable file. 180 """ 181 182 path = self.full_path(key) 183 edit_path = self.edit_path(key) 184 185 # Attempt to lock the file by renaming it. 186 # NOTE: This assumes that renaming is an atomic operation. 187 188 if os.path.exists(path) or os.path.exists(edit_path): 189 while 1: 190 try: 191 os.rename(path, edit_path) 192 except OSError: 193 time.sleep(self.delay) 194 else: 195 break 196 197 # Where a file does not exist, attempt to create a new file. 198 # Since file creation is probably not atomic, we use the renaming of a 199 # special file in an attempt to impose some kind of atomic "bottleneck". 200 201 elif create: 202 203 # NOTE: Avoid failure case where no __new__ file exists for some 204 # NOTE: reason. 205 206 try: 207 self.lock(self.new_filename) 208 except KeyError: 209 f = open(self.edit_path(self.new_filename), "wb") 210 f.close() 211 212 try: 213 if opener is None: 214 f = open(edit_path, "wb") 215 f.close() 216 else: 217 f = opener(edit_path) 218 f.close() 219 finally: 220 self.unlock(self.new_filename) 221 222 # Where no creation is requested, raise an exception. 223 224 else: 225 raise KeyError, key 226 227 return edit_path 228 229 def unlock(self, key): 230 231 """ 232 Unlock the file associated with the given 'key'. 233 234 Important note: this method should be used in a finally clause in order 235 to avoid files being locked and never being unlocked by the same process 236 because an unhandled exception was raised. 237 """ 238 239 path = self.full_path(key) 240 edit_path = self.edit_path(key) 241 os.rename(edit_path, path) 242 243 def __delitem__(self, key): 244 245 "Remove the file associated with the given 'key'." 246 247 path = self.full_path(key) 248 edit_path = self.edit_path(key) 249 if os.path.exists(path) or os.path.exists(edit_path): 250 while 1: 251 try: 252 os.remove(path) 253 except OSError: 254 time.sleep(self.delay) 255 else: 256 break 257 else: 258 raise KeyError, key 259 260 def __getitem__(self, key): 261 262 "Return the contents of the file associated with the given 'key'." 263 264 edit_path = self.lock(key, create=0) 265 try: 266 f = open(edit_path, "rb") 267 s = "" 268 try: 269 s = f.read() 270 finally: 271 f.close() 272 finally: 273 self.unlock(key) 274 275 return s 276 277 def __setitem__(self, key, value): 278 279 """ 280 Set the contents of the file associated with the given 'key' using the 281 given 'value'. 282 """ 283 284 edit_path = self.lock(key, create=1) 285 try: 286 f = open(edit_path, "wb") 287 try: 288 f.write(value) 289 finally: 290 f.close() 291 finally: 292 self.unlock(key) 293 294 # vim: tabstop=4 expandtab shiftwidth=4