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 os.mkdir(path) 60 self.create_resource(self.full_path(self.new_filename)) 61 62 # Guess the filesystem encoding. 63 64 if fsencoding is None: 65 if os.path.supports_unicode_filenames: 66 self.fsencoding = None 67 else: 68 import locale 69 self.fsencoding = locale.getdefaultlocale()[1] or 'ascii' 70 71 # Or override any guesses. 72 73 else: 74 self.fsencoding = fsencoding or 'ascii' 75 76 def _convert_name(self, name): 77 78 "Convert the given 'name' to a plain string in the filesystem encoding." 79 80 if self.fsencoding: 81 return name.encode(self.fsencoding) 82 else: 83 return name 84 85 def _convert_fsname(self, name): 86 87 """ 88 Convert the given 'name' as a plain string in the filesystem encoding to 89 a Unicode object. 90 """ 91 92 if self.fsencoding: 93 return unicode(name, self.fsencoding) 94 else: 95 return name 96 97 def keys(self): 98 99 "Return the names of the files stored in the repository." 100 101 # NOTE: Special names converted using a simple rule. 102 l = [] 103 for name in os.listdir(self.path): 104 if name.endswith(".edit"): 105 name = name[:-5] 106 if name != self.new_filename: 107 l.append(name) 108 return map(self._convert_fsname, l) 109 110 def full_path(self, key, edit=0): 111 112 """ 113 Return the full path to the 'key' in the filesystem. If the optional 114 'edit' parameter is set to a true value, the returned path will refer to 115 the editable version of the file. 116 """ 117 118 # NOTE: Special names converted using a simple rule. 119 path = os.path.abspath(os.path.join(self.path, self._convert_name(key))) 120 if edit: 121 path = path + ".edit" 122 if not path.startswith(self.path): 123 raise ValueError, key 124 else: 125 return path 126 127 def edit_path(self, key): 128 129 """ 130 Return the full path to the 'key' in the filesystem provided that the 131 file associated with the 'key' is locked for editing. 132 """ 133 134 return self.full_path(key, edit=1) 135 136 def has_key(self, key): 137 138 """ 139 Return whether a file with the name specified by 'key is stored in the 140 repository. 141 """ 142 143 return key in self.keys() 144 145 # NOTE: Methods very similar to Helpers.Session.Wrapper. 146 147 def items(self): 148 149 """ 150 Return a list of (name, value) tuples for the files stored in the 151 repository. 152 """ 153 154 results = [] 155 for key in self.keys(): 156 results.append((key, self[key])) 157 return results 158 159 def values(self): 160 161 "Return the contents of the files stored in the repository." 162 163 results = [] 164 for key in self.keys(): 165 results.append(self[key]) 166 return results 167 168 def lock(self, key, create=0, opener=None): 169 170 """ 171 Lock the file associated with the given 'key'. If the optional 'create' 172 parameter is set to a true value (unlike the default), the file will be 173 created if it did not already exist; otherwise, a KeyError will be 174 raised. 175 176 If the optional 'opener' parameter is specified, it will be used to 177 create any new file in the case where 'create' is set to a true value; 178 otherwise, the standard 'open' function will be used to create the file. 179 180 Return the full path to the editable file. 181 """ 182 183 path = self.full_path(key) 184 edit_path = self.edit_path(key) 185 186 # Attempt to lock the file by renaming it. 187 # NOTE: This assumes that renaming is an atomic operation. 188 189 if os.path.exists(path) or os.path.exists(edit_path): 190 while 1: 191 try: 192 os.rename(path, edit_path) 193 except OSError: 194 time.sleep(self.delay) 195 else: 196 break 197 198 # Where a file does not exist, attempt to create a new file. 199 # Since file creation is probably not atomic, we use the renaming of a 200 # special file in an attempt to impose some kind of atomic "bottleneck". 201 202 elif create: 203 204 # NOTE: Avoid failure case where no __new__ file exists for some 205 # NOTE: reason. 206 207 try: 208 self.lock(self.new_filename) 209 except KeyError: 210 self.create_resource(self.edit_path(self.new_filename)) 211 212 try: 213 self.create_data(edit_path, opener) 214 finally: 215 self.unlock(self.new_filename) 216 217 # Where no creation is requested, raise an exception. 218 219 else: 220 raise KeyError, key 221 222 return edit_path 223 224 def unlock(self, key): 225 226 """ 227 Unlock the file associated with the given 'key'. 228 229 Important note: this method should be used in a finally clause in order 230 to avoid files being locked and never being unlocked by the same process 231 because an unhandled exception was raised. 232 """ 233 234 path = self.full_path(key) 235 edit_path = self.edit_path(key) 236 os.rename(edit_path, path) 237 238 def __delitem__(self, key): 239 240 "Remove the file associated with the given 'key'." 241 242 edit_path = self.lock(key) 243 self.remove_resource(edit_path) 244 245 def __getitem__(self, key): 246 247 "Return the contents of the file associated with the given 'key'." 248 249 edit_path = self.lock(key, create=0) 250 try: 251 f = self.open_resource(edit_path, "rb") 252 s = "" 253 try: 254 s = f.read() 255 finally: 256 f.close() 257 finally: 258 self.unlock(key) 259 260 return s 261 262 def __setitem__(self, key, value): 263 264 """ 265 Set the contents of the file associated with the given 'key' using the 266 given 'value'. 267 """ 268 269 edit_path = self.lock(key, create=1) 270 try: 271 f = self.open_resource(edit_path, "wb") 272 try: 273 f.write(value) 274 finally: 275 f.close() 276 finally: 277 self.unlock(key) 278 279 def create_resource(self, filename): 280 f = open(filename, "wb") 281 f.close() 282 283 def create_data(self, edit_path, opener): 284 if opener is None: 285 f = open(edit_path, "wb") 286 f.close() 287 else: 288 f = opener(edit_path) 289 f.close() 290 291 def remove_resource(self, edit_path): 292 os.remove(edit_path) 293 294 def open_resource(self, edit_path, mode): 295 return open(edit_path, mode) 296 297 class SessionDirectoryRepository(DirectoryRepository): 298 299 def create_resource(self, filename): 300 os.mkdir(filename) 301 302 def create_data(self, edit_path, opener): 303 os.mkdir(edit_path) 304 if opener is None: 305 f = open(os.path.join(edit_path, "data"), "wb") 306 f.close() 307 else: 308 f = opener(os.path.join(edit_path, "data")) 309 f.close() 310 311 def remove_resource(self, edit_path): 312 for filename in glob.glob(os.path.join(edit_path, "*")): 313 os.remove(filename) 314 os.rmdir(edit_path) 315 316 def open_resource(self, edit_path, mode): 317 return open(os.path.join(edit_path, "data"), mode) 318 319 # vim: tabstop=4 expandtab shiftwidth=4