paulb@498 | 1 | #!/usr/bin/env python |
paulb@498 | 2 | |
paulb@498 | 3 | """ |
paulb@498 | 4 | Directory repositories for WebStack. |
paulb@498 | 5 | |
paulb@498 | 6 | Copyright (C) 2005 Paul Boddie <paul@boddie.org.uk> |
paulb@498 | 7 | |
paulb@498 | 8 | This library is free software; you can redistribute it and/or |
paulb@498 | 9 | modify it under the terms of the GNU Lesser General Public |
paulb@498 | 10 | License as published by the Free Software Foundation; either |
paulb@498 | 11 | version 2.1 of the License, or (at your option) any later version. |
paulb@498 | 12 | |
paulb@498 | 13 | This library is distributed in the hope that it will be useful, |
paulb@498 | 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
paulb@498 | 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
paulb@498 | 16 | Lesser General Public License for more details. |
paulb@498 | 17 | |
paulb@498 | 18 | You should have received a copy of the GNU Lesser General Public |
paulb@498 | 19 | License along with this library; if not, write to the Free Software |
paulb@498 | 20 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
paulb@498 | 21 | """ |
paulb@498 | 22 | |
paulb@498 | 23 | import os |
paulb@498 | 24 | |
paulb@498 | 25 | class DirectoryRepository: |
paulb@498 | 26 | |
paulb@498 | 27 | "A directory repository providing session-like access to files." |
paulb@498 | 28 | |
paulb@511 | 29 | def __init__(self, path, fsencoding=None, delay=1): |
paulb@498 | 30 | |
paulb@498 | 31 | """ |
paulb@498 | 32 | Initialise the repository using the given 'path' to indicate the |
paulb@498 | 33 | location of the repository. If no such location exists in the filesystem |
paulb@498 | 34 | an attempt will be made to create the directory. |
paulb@498 | 35 | |
paulb@498 | 36 | The optional 'fsencoding' parameter can be used to assert a particular |
paulb@498 | 37 | character encoding used by the filesystem to represent filenames. By |
paulb@498 | 38 | default, the default encoding is detected (or Unicode objects are used |
paulb@498 | 39 | if appropriate). |
paulb@511 | 40 | |
paulb@511 | 41 | The optional 'delay' argument specifies the time in seconds between each |
paulb@511 | 42 | poll of an opened repository file when that file is found to be locked |
paulb@511 | 43 | for editing. |
paulb@498 | 44 | """ |
paulb@498 | 45 | |
paulb@498 | 46 | if not os.path.exists(path): |
paulb@498 | 47 | os.mkdir(path) |
paulb@498 | 48 | self.path = path |
paulb@498 | 49 | self.fsencoding = fsencoding |
paulb@511 | 50 | self.delay = delay |
paulb@498 | 51 | |
paulb@498 | 52 | # Guess the filesystem encoding. |
paulb@498 | 53 | |
paulb@498 | 54 | if fsencoding is None: |
paulb@498 | 55 | if os.path.supports_unicode_filenames: |
paulb@498 | 56 | self.fsencoding = None |
paulb@498 | 57 | else: |
paulb@498 | 58 | import locale |
paulb@498 | 59 | self.fsencoding = locale.getdefaultlocale()[1] |
paulb@498 | 60 | |
paulb@498 | 61 | # Or override any guesses. |
paulb@498 | 62 | |
paulb@498 | 63 | else: |
paulb@498 | 64 | self.fsencoding = fsencoding |
paulb@498 | 65 | |
paulb@498 | 66 | def _convert_name(self, name): |
paulb@498 | 67 | if self.fsencoding: |
paulb@498 | 68 | return name.encode(self.fsencoding) |
paulb@498 | 69 | else: |
paulb@498 | 70 | return name |
paulb@498 | 71 | |
paulb@498 | 72 | def _convert_fsname(self, name): |
paulb@498 | 73 | if self.fsencoding: |
paulb@498 | 74 | return unicode(name, self.fsencoding) |
paulb@498 | 75 | else: |
paulb@498 | 76 | return name |
paulb@498 | 77 | |
paulb@498 | 78 | def keys(self): |
paulb@511 | 79 | # NOTE: Special names converted using a simple rule. |
paulb@511 | 80 | l = [] |
paulb@511 | 81 | for name in os.listdir(self.path): |
paulb@511 | 82 | if name.endswith(".edit"): |
paulb@511 | 83 | l.append(name[:-5]) |
paulb@511 | 84 | else: |
paulb@511 | 85 | l.append(name) |
paulb@511 | 86 | return map(self._convert_fsname, l) |
paulb@498 | 87 | |
paulb@498 | 88 | def full_path(self, key): |
paulb@513 | 89 | path = os.path.abspath(os.path.join(self.path, self._convert_name(key))) |
paulb@513 | 90 | if not path.startswith(self.path): |
paulb@513 | 91 | raise ValueError, key |
paulb@513 | 92 | else: |
paulb@513 | 93 | return path |
paulb@498 | 94 | |
paulb@503 | 95 | def has_key(self, key): |
paulb@503 | 96 | return key in self.keys() |
paulb@503 | 97 | |
paulb@498 | 98 | # NOTE: Methods very similar to Helpers.Session.Wrapper. |
paulb@498 | 99 | |
paulb@498 | 100 | def items(self): |
paulb@498 | 101 | results = [] |
paulb@498 | 102 | for key in self.keys(): |
paulb@498 | 103 | results.append((key, self[key])) |
paulb@498 | 104 | return results |
paulb@498 | 105 | |
paulb@498 | 106 | def values(self): |
paulb@498 | 107 | results = [] |
paulb@498 | 108 | for key in self.keys(): |
paulb@498 | 109 | results.append(self[key]) |
paulb@498 | 110 | return results |
paulb@498 | 111 | |
paulb@513 | 112 | def lock(self, key, create=0): |
paulb@513 | 113 | |
paulb@513 | 114 | """ |
paulb@513 | 115 | Lock the file associated with the given 'key'. If the optional 'create' |
paulb@513 | 116 | parameter is set to a true value (unlike the default), the file will be |
paulb@513 | 117 | created if it did not already exist; otherwise, a KeyError will be |
paulb@513 | 118 | raised. |
paulb@513 | 119 | |
paulb@513 | 120 | Return the full path to the file. |
paulb@513 | 121 | """ |
paulb@513 | 122 | |
paulb@511 | 123 | filename = self.full_path(key) |
paulb@511 | 124 | if os.path.exists(filename) or os.path.exists(filename + ".edit"): |
paulb@511 | 125 | while 1: |
paulb@511 | 126 | try: |
paulb@511 | 127 | os.rename(filename, filename + ".edit") |
paulb@511 | 128 | except OSError: |
paulb@511 | 129 | time.sleep(self.delay) |
paulb@511 | 130 | else: |
paulb@511 | 131 | break |
paulb@513 | 132 | elif create: |
paulb@513 | 133 | f = open(filename + ".edit", "wb") |
paulb@513 | 134 | f.close() |
paulb@511 | 135 | else: |
paulb@513 | 136 | raise KeyError, key |
paulb@513 | 137 | return filename |
paulb@513 | 138 | |
paulb@513 | 139 | def unlock(self, key): |
paulb@513 | 140 | |
paulb@513 | 141 | """ |
paulb@513 | 142 | Unlock the file associated with the given 'key'. |
paulb@513 | 143 | |
paulb@513 | 144 | Important note: this method should be used in a finally clause in order |
paulb@513 | 145 | to avoid files being locked and never being unlocked by the same process |
paulb@513 | 146 | because an unhandled exception was raised. |
paulb@513 | 147 | """ |
paulb@513 | 148 | |
paulb@513 | 149 | filename = self.full_path(key) |
paulb@513 | 150 | os.rename(filename + ".edit", filename) |
paulb@498 | 151 | |
paulb@498 | 152 | def __delitem__(self, key): |
paulb@511 | 153 | filename = self.full_path(key) |
paulb@511 | 154 | if os.path.exists(filename) or os.path.exists(filename + ".edit"): |
paulb@511 | 155 | while 1: |
paulb@511 | 156 | try: |
paulb@511 | 157 | os.remove(filename) |
paulb@511 | 158 | except OSError: |
paulb@511 | 159 | time.sleep(self.delay) |
paulb@511 | 160 | else: |
paulb@511 | 161 | break |
paulb@511 | 162 | else: |
paulb@511 | 163 | raise KeyError, key |
paulb@511 | 164 | |
paulb@511 | 165 | def __getitem__(self, key): |
paulb@513 | 166 | filename = self.lock(key, create=0) |
paulb@511 | 167 | try: |
paulb@511 | 168 | f = open(filename + ".edit", "rb") |
paulb@511 | 169 | s = "" |
paulb@511 | 170 | try: |
paulb@511 | 171 | s = f.read() |
paulb@511 | 172 | finally: |
paulb@511 | 173 | f.close() |
paulb@511 | 174 | finally: |
paulb@513 | 175 | self.unlock(key) |
paulb@511 | 176 | |
paulb@511 | 177 | return s |
paulb@498 | 178 | |
paulb@498 | 179 | def __setitem__(self, key, value): |
paulb@513 | 180 | filename = self.lock(key, create=1) |
paulb@498 | 181 | try: |
paulb@511 | 182 | f = open(filename + ".edit", "wb") |
paulb@511 | 183 | try: |
paulb@511 | 184 | f.write(value) |
paulb@511 | 185 | finally: |
paulb@511 | 186 | f.close() |
paulb@498 | 187 | finally: |
paulb@513 | 188 | self.unlock(key) |
paulb@498 | 189 | |
paulb@498 | 190 | # vim: tabstop=4 expandtab shiftwidth=4 |