1 #!/usr/bin/env python 2 3 """ 4 Directory repositories for WebStack. 5 6 Copyright (C) 2005, 2006, 2007 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 import time 25 import glob 26 27 class DirectoryRepository: 28 29 "A directory repository providing session-like access to files." 30 31 new_filename = "__new__" 32 33 def __init__(self, path, fsencoding=None, delay=1): 34 35 """ 36 Initialise the repository using the given 'path' to indicate the 37 location of the repository. If no such location exists in the filesystem 38 an attempt will be made to create the directory. 39 40 The optional 'fsencoding' parameter can be used to assert a particular 41 character encoding used by the filesystem to represent filenames. By 42 default, the default encoding is detected (or Unicode objects are used 43 if appropriate). 44 45 The optional 'delay' argument specifies the time in seconds between each 46 poll of an opened repository file when that file is found to be locked 47 for editing. 48 """ 49 50 # Convert the path to an absolute path. 51 52 self.path = os.path.abspath(path) 53 self.fsencoding = fsencoding 54 self.delay = delay 55 56 # Create a directory and initialise it with a special file. 57 58 if not os.path.exists(path): 59 self.create_resource(self.full_path(self.new_filename)) 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] or 'ascii' 69 70 # Or override any guesses. 71 72 else: 73 self.fsencoding = fsencoding or 'ascii' 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 self.create_resource(self.edit_path(self.new_filename)) 210 211 try: 212 self.create_data(edit_path, opener) 213 finally: 214 self.unlock(self.new_filename) 215 216 # Where no creation is requested, raise an exception. 217 218 else: 219 raise KeyError, key 220 221 return edit_path 222 223 def unlock(self, key): 224 225 """ 226 Unlock the file associated with the given 'key'. 227 228 Important note: this method should be used in a finally clause in order 229 to avoid files being locked and never being unlocked by the same process 230 because an unhandled exception was raised. 231 """ 232 233 path = self.full_path(key) 234 edit_path = self.edit_path(key) 235 os.rename(edit_path, path) 236 237 def __delitem__(self, key): 238 239 "Remove the file associated with the given 'key'." 240 241 edit_path = self.lock(key) 242 self.remove_resource(edit_path) 243 244 def __getitem__(self, key): 245 246 "Return the contents of the file associated with the given 'key'." 247 248 edit_path = self.lock(key, create=0) 249 try: 250 f = self.open_resource(edit_path, "rb") 251 s = "" 252 try: 253 s = f.read() 254 finally: 255 f.close() 256 finally: 257 self.unlock(key) 258 259 return s 260 261 def __setitem__(self, key, value): 262 263 """ 264 Set the contents of the file associated with the given 'key' using the 265 given 'value'. 266 """ 267 268 edit_path = self.lock(key, create=1) 269 try: 270 f = self.open_resource(edit_path, "wb") 271 try: 272 f.write(value) 273 finally: 274 f.close() 275 finally: 276 self.unlock(key) 277 278 def create_resource(self, filename): 279 f = open(filename, "wb") 280 f.close() 281 282 def create_data(self, edit_path, opener): 283 if opener is None: 284 f = open(edit_path, "wb") 285 f.close() 286 else: 287 f = opener(edit_path) 288 f.close() 289 290 def remove_resource(self, edit_path): 291 os.remove(edit_path) 292 293 def open_resource(self, edit_path, mode): 294 return open(edit_path, mode) 295 296 class SessionDirectoryRepository(DirectoryRepository): 297 298 def create_resource(self, filename): 299 os.mkdir(filename) 300 301 def create_data(self, edit_path, opener): 302 os.mkdir(edit_path) 303 if opener is None: 304 f = open(os.path.join(edit_path, "data"), "wb") 305 f.close() 306 else: 307 f = opener(os.path.join(edit_path, "data")) 308 f.close() 309 310 def remove_resource(self, edit_path): 311 for filename in glob.glob(os.path.join(edit_path, "*")): 312 os.remove(filename) 313 os.rmdir(edit_path) 314 315 def open_resource(self, edit_path, mode): 316 return open(os.path.join(edit_path, "data"), mode) 317 318 # vim: tabstop=4 expandtab shiftwidth=4