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