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 # NOTE: Add fsync-related stuff here due to ext4 and other filesystems? 237 os.rename(edit_path, path) 238 239 def __delitem__(self, key): 240 241 "Remove the file associated with the given 'key'." 242 243 edit_path = self.lock(key) 244 self.remove_resource(edit_path) 245 246 def __getitem__(self, key): 247 248 "Return the contents of the file associated with the given 'key'." 249 250 edit_path = self.lock(key, create=0) 251 try: 252 f = self.open_resource(edit_path, "rb") 253 s = "" 254 try: 255 s = f.read() 256 finally: 257 f.close() 258 finally: 259 self.unlock(key) 260 261 return s 262 263 def __setitem__(self, key, value): 264 265 """ 266 Set the contents of the file associated with the given 'key' using the 267 given 'value'. 268 """ 269 270 edit_path = self.lock(key, create=1) 271 try: 272 f = self.open_resource(edit_path, "wb") 273 try: 274 f.write(value) 275 finally: 276 f.close() 277 finally: 278 self.unlock(key) 279 280 def create_resource(self, filename): 281 f = open(filename, "wb") 282 f.close() 283 284 def create_data(self, edit_path, opener): 285 if opener is None: 286 f = open(edit_path, "wb") 287 f.close() 288 else: 289 f = opener(edit_path) 290 f.close() 291 292 def remove_resource(self, edit_path): 293 os.remove(edit_path) 294 295 def open_resource(self, edit_path, mode): 296 return open(edit_path, mode) 297 298 class SessionDirectoryRepository(DirectoryRepository): 299 300 def create_resource(self, filename): 301 os.mkdir(filename) 302 303 def create_data(self, edit_path, opener): 304 os.mkdir(edit_path) 305 if opener is None: 306 f = open(os.path.join(edit_path, "data"), "wb") 307 f.close() 308 else: 309 f = opener(os.path.join(edit_path, "data")) 310 f.close() 311 312 def remove_resource(self, edit_path): 313 for filename in glob.glob(os.path.join(edit_path, "*")): 314 os.remove(filename) 315 os.rmdir(edit_path) 316 317 def open_resource(self, edit_path, mode): 318 return open(os.path.join(edit_path, "data"), mode) 319 320 # vim: tabstop=4 expandtab shiftwidth=4