1 #!/usr/bin/env python 2 3 """ 4 A simple filesystem-based store of calendar data. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from datetime import datetime 23 from imiptools.config import STORE_DIR, PUBLISH_DIR 24 from os.path import abspath, commonprefix, exists, isfile, join, split 25 from os import chmod, listdir, makedirs 26 from vCalendar import iterwrite 27 28 def check_dir(base, dir): 29 return commonprefix([base, abspath(dir)]) == base 30 31 def fix_permissions(filename): 32 try: 33 chmod(filename, 0660) 34 except OSError: 35 pass 36 37 def make_calendar(fragment, method=None): 38 39 """ 40 Return a complete calendar item wrapping the given 'fragment' and employing 41 the given 'method', if indicated. 42 """ 43 44 return ("VCALENDAR", {}, 45 (method and [("METHOD", {}, method)] or []) + 46 [("VERSION", {}, "2.0")] + 47 fragment 48 ) 49 50 def to_stream(out, fragment, encoding="utf-8"): 51 iterwrite(out, encoding=encoding).append(fragment) 52 53 class FileBase: 54 55 "Basic filesystem operations." 56 57 def __init__(self, store_dir=STORE_DIR): 58 self.store_dir = store_dir 59 if not exists(self.store_dir): 60 makedirs(self.store_dir) 61 62 def get_file_object(self, base, *parts): 63 pathname = join(base, *parts) 64 return check_dir(base, pathname) and pathname or None 65 66 def get_object_in_store(self, *parts): 67 68 """ 69 Return the name of any valid object stored within a hierarchy specified 70 by the given 'parts'. 71 """ 72 73 parent = expected = self.store_dir 74 75 for part in parts: 76 filename = self.get_file_object(expected, part) 77 if not filename: 78 return False 79 parent = expected 80 expected = filename 81 82 if not exists(parent): 83 makedirs(parent) 84 85 return filename 86 87 class FileStore(FileBase): 88 89 "A file store of tabular free/busy data and objects." 90 91 def get_events(self, user): 92 93 "Return a list of event identifiers." 94 95 filename = self.get_object_in_store(user, "objects") 96 if not filename or not exists(filename): 97 return None 98 99 return [name for name in listdir(filename) if isfile(join(filename, name))] 100 101 def get_event(self, user, uid): 102 103 "Get the event for the given 'user' with the given 'uid'." 104 105 filename = self.get_object_in_store(user, "objects", uid) 106 if not filename or not exists(filename): 107 return None 108 109 return open(filename) or None 110 111 def set_event(self, user, uid, node): 112 113 "Set an event for 'user' having the given 'uid' and 'node'." 114 115 filename = self.get_object_in_store(user, "objects", uid) 116 if not filename: 117 return False 118 119 f = open(filename, "w") 120 try: 121 to_stream(f, node) 122 finally: 123 f.close() 124 fix_permissions(filename) 125 126 return True 127 128 def get_freebusy(self, user): 129 130 "Get free/busy details for the given 'user'." 131 132 filename = self.get_object_in_store(user, "freebusy") 133 if not filename or not exists(filename): 134 return None 135 else: 136 return self._get_freebusy(filename) 137 138 def get_freebusy_for_other(self, user, other): 139 140 "For the given 'user', get free/busy details for the 'other' user." 141 142 filename = self.get_object_in_store(user, "freebusy-other", other) 143 if not filename: 144 return None 145 else: 146 return self._get_freebusy(filename) 147 148 def _get_freebusy(self, filename): 149 f = open(filename) 150 try: 151 l = [] 152 for line in f.readlines(): 153 l.append(tuple(line.strip().split("\t"))) 154 return l 155 finally: 156 f.close() 157 158 def set_freebusy(self, user, freebusy): 159 160 "For the given 'user', set 'freebusy' details." 161 162 filename = self.get_object_in_store(user, "freebusy") 163 if not filename: 164 return False 165 166 self._set_freebusy(filename, freebusy) 167 return True 168 169 def set_freebusy_for_other(self, user, freebusy, other): 170 171 "For the given 'user', set 'freebusy' details for the 'other' user." 172 173 filename = self.get_object_in_store(user, "freebusy-other", other) 174 if not filename: 175 return False 176 177 self._set_freebusy(filename, freebusy) 178 return True 179 180 def _set_freebusy(self, filename, freebusy): 181 f = open(filename, "w") 182 try: 183 for item in freebusy: 184 f.write("\t".join([(value or "OPAQUE") for value in item]) + "\n") 185 finally: 186 f.close() 187 fix_permissions(filename) 188 189 def _get_requests(self, user, queue): 190 191 "Get requests for the given 'user' from the given 'queue'." 192 193 filename = self.get_object_in_store(user, queue) 194 if not filename or not exists(filename): 195 return None 196 197 f = open(filename) 198 try: 199 return [line.strip() for line in f.readlines()] 200 finally: 201 f.close() 202 203 def get_requests(self, user): 204 205 "Get requests for the given 'user'." 206 207 return self._get_requests(user, "requests") 208 209 def get_cancellations(self, user): 210 211 "Get cancellations for the given 'user'." 212 213 return self._get_requests(user, "cancellations") 214 215 def _set_requests(self, user, requests, queue): 216 217 """ 218 For the given 'user', set the list of queued 'requests' in the given 219 'queue'. 220 """ 221 222 filename = self.get_object_in_store(user, queue) 223 if not filename: 224 return False 225 226 f = open(filename, "w") 227 try: 228 for request in requests: 229 print >>f, request 230 finally: 231 f.close() 232 fix_permissions(filename) 233 234 return True 235 236 def set_requests(self, user, requests): 237 238 "For the given 'user', set the list of queued 'requests'." 239 240 return self._set_requests(user, requests, "requests") 241 242 def set_cancellations(self, user, cancellations): 243 244 "For the given 'user', set the list of queued 'cancellations'." 245 246 return self._set_requests(user, cancellations, "cancellations") 247 248 def _set_request(self, user, request, queue): 249 250 "For the given 'user', set the queued 'request' in the given 'queue'." 251 252 filename = self.get_object_in_store(user, queue) 253 if not filename: 254 return False 255 256 f = open(filename, "a") 257 try: 258 print >>f, request 259 finally: 260 f.close() 261 fix_permissions(filename) 262 263 return True 264 265 def set_request(self, user, request): 266 267 "For the given 'user', set the queued 'request'." 268 269 return self._set_request(user, request, "requests") 270 271 def set_cancellation(self, user, cancellation): 272 273 "For the given 'user', set the queued 'cancellation'." 274 275 return self._set_request(user, cancellation, "cancellations") 276 277 def queue_request(self, user, uid): 278 279 "Queue a request for 'user' having the given 'uid'." 280 281 requests = self.get_requests(user) or [] 282 283 if uid not in requests: 284 return self.set_request(user, uid) 285 286 return False 287 288 def dequeue_request(self, user, uid): 289 290 "Dequeue a request for 'user' having the given 'uid'." 291 292 requests = self.get_requests(user) or [] 293 294 try: 295 requests.remove(uid) 296 self.set_requests(user, requests) 297 except ValueError: 298 return False 299 else: 300 return True 301 302 def cancel_event(self, user, uid): 303 304 "Queue an event for cancellation for 'user' having the given 'uid'." 305 306 cancellations = self.get_cancellations(user) or [] 307 308 if uid not in cancellations: 309 return self.set_cancellation(user, uid) 310 311 return False 312 313 class FilePublisher(FileBase): 314 315 "A publisher of objects." 316 317 def __init__(self, store_dir=PUBLISH_DIR): 318 FileBase.__init__(self, store_dir) 319 320 def set_freebusy(self, user, freebusy): 321 322 "For the given 'user', set 'freebusy' details." 323 324 filename = self.get_object_in_store(user, "freebusy") 325 if not filename: 326 return False 327 328 record = [] 329 rwrite = record.append 330 331 rwrite(("ORGANIZER", {}, user)) 332 rwrite(("UID", {}, user)) 333 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 334 335 for start, end, uid, transp in freebusy: 336 if not transp or transp == "OPAQUE": 337 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) 338 339 f = open(filename, "w") 340 try: 341 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 342 finally: 343 f.close() 344 fix_permissions(filename) 345 346 return True 347 348 # vim: tabstop=4 expandtab shiftwidth=4