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