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 imiptools.data import make_calendar, parse_object, to_stream 25 from imiptools.filesys import fix_permissions, FileBase 26 from os.path import exists, isfile, join 27 from os import listdir, remove, rmdir 28 from time import sleep 29 30 class FileStore(FileBase): 31 32 "A file store of tabular free/busy data and objects." 33 34 def __init__(self, store_dir=STORE_DIR): 35 FileBase.__init__(self, store_dir) 36 37 def acquire_lock(self, user, timeout=None): 38 FileBase.acquire_lock(self, timeout, user) 39 40 def release_lock(self, user): 41 FileBase.release_lock(self, user) 42 43 def _set_defaults(self, t, empty_defaults): 44 for i, default in empty_defaults: 45 if i >= len(t): 46 t += [None] * (i - len(t) + 1) 47 if not t[i]: 48 t[i] = default 49 return t 50 51 def _get_table(self, user, filename, empty_defaults=None): 52 53 """ 54 From the file for the given 'user' having the given 'filename', return 55 a list of tuples representing the file's contents. 56 57 The 'empty_defaults' is a list of (index, value) tuples indicating the 58 default value where a column either does not exist or provides an empty 59 value. 60 """ 61 62 self.acquire_lock(user) 63 try: 64 f = open(filename, "rb") 65 try: 66 l = [] 67 for line in f.readlines(): 68 t = line.strip().split("\t") 69 if empty_defaults: 70 t = self._set_defaults(t, empty_defaults) 71 l.append(tuple(t)) 72 return l 73 finally: 74 f.close() 75 finally: 76 self.release_lock(user) 77 78 def _set_table(self, user, filename, items, empty_defaults=None): 79 80 """ 81 For the given 'user', write to the file having the given 'filename' the 82 'items'. 83 84 The 'empty_defaults' is a list of (index, value) tuples indicating the 85 default value where a column either does not exist or provides an empty 86 value. 87 """ 88 89 self.acquire_lock(user) 90 try: 91 f = open(filename, "wb") 92 try: 93 for item in items: 94 if empty_defaults: 95 item = self._set_defaults(list(item), empty_defaults) 96 f.write("\t".join(item) + "\n") 97 finally: 98 f.close() 99 fix_permissions(filename) 100 finally: 101 self.release_lock(user) 102 103 def _get_object(self, user, filename): 104 105 """ 106 Return the parsed object for the given 'user' having the given 107 'filename'. 108 """ 109 110 self.acquire_lock(user) 111 try: 112 f = open(filename, "rb") 113 try: 114 return parse_object(f, "utf-8") 115 finally: 116 f.close() 117 finally: 118 self.release_lock(user) 119 120 def _set_object(self, user, filename, node): 121 122 """ 123 Set an object for the given 'user' having the given 'filename', using 124 'node' to define the object. 125 """ 126 127 self.acquire_lock(user) 128 try: 129 f = open(filename, "wb") 130 try: 131 to_stream(f, node) 132 finally: 133 f.close() 134 fix_permissions(filename) 135 finally: 136 self.release_lock(user) 137 138 return True 139 140 def _remove_object(self, filename): 141 142 "Remove the object with the given 'filename'." 143 144 try: 145 remove(filename) 146 except OSError: 147 return False 148 149 return True 150 151 def _remove_collection(self, filename): 152 153 "Remove the collection with the given 'filename'." 154 155 try: 156 rmdir(filename) 157 except OSError: 158 return False 159 160 return True 161 162 def get_events(self, user): 163 164 "Return a list of event identifiers." 165 166 filename = self.get_object_in_store(user, "objects") 167 if not filename or not exists(filename): 168 return None 169 170 return [name for name in listdir(filename) if isfile(join(filename, name))] 171 172 def get_event(self, user, uid, recurrenceid=None): 173 174 """ 175 Get the event for the given 'user' with the given 'uid'. If 176 the optional 'recurrenceid' is specified, a specific instance or 177 occurrence of an event is returned. 178 """ 179 180 if recurrenceid: 181 return self.get_recurrence(user, uid, recurrenceid) 182 else: 183 return self.get_complete_event(user, uid) 184 185 def get_complete_event(self, user, uid): 186 187 "Get the event for the given 'user' with the given 'uid'." 188 189 filename = self.get_object_in_store(user, "objects", uid) 190 if not filename or not exists(filename): 191 return None 192 193 return self._get_object(user, filename) 194 195 def set_event(self, user, uid, recurrenceid, node): 196 197 """ 198 Set an event for 'user' having the given 'uid' and 'recurrenceid' (which 199 if the latter is specified, a specific instance or occurrence of an 200 event is referenced), using the given 'node' description. 201 """ 202 203 if recurrenceid: 204 return self.set_recurrence(user, uid, recurrenceid, node) 205 else: 206 return self.set_complete_event(user, uid, node) 207 208 def set_complete_event(self, user, uid, node): 209 210 "Set an event for 'user' having the given 'uid' and 'node'." 211 212 filename = self.get_object_in_store(user, "objects", uid) 213 if not filename: 214 return False 215 216 return self._set_object(user, filename, node) 217 218 def remove_event(self, user, uid, recurrenceid=None): 219 220 """ 221 Remove an event for 'user' having the given 'uid'. If the optional 222 'recurrenceid' is specified, a specific instance or occurrence of an 223 event is removed. 224 """ 225 226 if recurrenceid: 227 return self.remove_recurrence(user, uid, recurrenceid) 228 else: 229 for recurrenceid in self.get_recurrences(user, uid) or []: 230 self.remove_recurrence(user, uid, recurrenceid) 231 return self.remove_complete_event(user, uid) 232 233 def remove_complete_event(self, user, uid): 234 235 "Remove an event for 'user' having the given 'uid'." 236 237 recurrences = self.get_object_in_store(user, "recurrences", uid) 238 if recurrences: 239 self._remove_collection(recurrences) 240 241 filename = self.get_object_in_store(user, "objects", uid) 242 if not filename: 243 return False 244 245 return self._remove_object(filename) 246 247 def get_recurrences(self, user, uid): 248 249 """ 250 Get additional event instances for an event of the given 'user' with the 251 indicated 'uid'. 252 """ 253 254 filename = self.get_object_in_store(user, "recurrences", uid) 255 if not filename or not exists(filename): 256 return [] 257 258 return [name for name in listdir(filename) if isfile(join(filename, name))] 259 260 def get_recurrence(self, user, uid, recurrenceid): 261 262 """ 263 For the event of the given 'user' with the given 'uid', return the 264 specific recurrence indicated by the 'recurrenceid'. 265 """ 266 267 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 268 if not filename or not exists(filename): 269 return None 270 271 return self._get_object(user, filename) 272 273 def set_recurrence(self, user, uid, recurrenceid, node): 274 275 "Set an event for 'user' having the given 'uid' and 'node'." 276 277 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 278 if not filename: 279 return False 280 281 return self._set_object(user, filename, node) 282 283 def remove_recurrence(self, user, uid, recurrenceid): 284 285 "Remove an event for 'user' having the given 'uid'." 286 287 filename = self.get_object_in_store(user, "recurrences", uid) 288 if not filename: 289 return False 290 291 return self._remove_object(filename) 292 293 def get_freebusy(self, user): 294 295 "Get free/busy details for the given 'user'." 296 297 filename = self.get_object_in_store(user, "freebusy") 298 if not filename or not exists(filename): 299 return [] 300 else: 301 return self._get_table(user, filename, [(4, None)]) 302 303 def get_freebusy_for_other(self, user, other): 304 305 "For the given 'user', get free/busy details for the 'other' user." 306 307 filename = self.get_object_in_store(user, "freebusy-other", other) 308 if not filename or not exists(filename): 309 return [] 310 else: 311 return self._get_table(user, filename, [(4, None)]) 312 313 def set_freebusy(self, user, freebusy): 314 315 "For the given 'user', set 'freebusy' details." 316 317 filename = self.get_object_in_store(user, "freebusy") 318 if not filename: 319 return False 320 321 self._set_table(user, filename, freebusy, [(3, "OPAQUE"), (4, "")]) 322 return True 323 324 def set_freebusy_for_other(self, user, freebusy, other): 325 326 "For the given 'user', set 'freebusy' details for the 'other' user." 327 328 filename = self.get_object_in_store(user, "freebusy-other", other) 329 if not filename: 330 return False 331 332 self._set_table(user, filename, freebusy, [(2, ""), (3, "OPAQUE"), (4, "")]) 333 return True 334 335 def _get_requests(self, user, queue): 336 337 "Get requests for the given 'user' from the given 'queue'." 338 339 filename = self.get_object_in_store(user, queue) 340 if not filename or not exists(filename): 341 return None 342 343 return self._get_table(user, filename, [(1, None)]) 344 345 def get_requests(self, user): 346 347 "Get requests for the given 'user'." 348 349 return self._get_requests(user, "requests") 350 351 def get_cancellations(self, user): 352 353 "Get cancellations for the given 'user'." 354 355 return self._get_requests(user, "cancellations") 356 357 def _set_requests(self, user, requests, queue): 358 359 """ 360 For the given 'user', set the list of queued 'requests' in the given 361 'queue'. 362 """ 363 364 filename = self.get_object_in_store(user, queue) 365 if not filename: 366 return False 367 368 self.acquire_lock(user) 369 try: 370 f = open(filename, "w") 371 try: 372 for request in requests: 373 print >>f, "\t".join([value or "" for value in request]) 374 finally: 375 f.close() 376 fix_permissions(filename) 377 finally: 378 self.release_lock(user) 379 380 return True 381 382 def set_requests(self, user, requests): 383 384 "For the given 'user', set the list of queued 'requests'." 385 386 return self._set_requests(user, requests, "requests") 387 388 def set_cancellations(self, user, cancellations): 389 390 "For the given 'user', set the list of queued 'cancellations'." 391 392 return self._set_requests(user, cancellations, "cancellations") 393 394 def _set_request(self, user, uid, recurrenceid, queue): 395 396 """ 397 For the given 'user', set the queued 'uid' and 'recurrenceid' in the 398 given 'queue'. 399 """ 400 401 filename = self.get_object_in_store(user, queue) 402 if not filename: 403 return False 404 405 self.acquire_lock(user) 406 try: 407 f = open(filename, "a") 408 try: 409 print >>f, "\t".join([uid, recurrenceid or ""]) 410 finally: 411 f.close() 412 fix_permissions(filename) 413 finally: 414 self.release_lock(user) 415 416 return True 417 418 def set_request(self, user, uid, recurrenceid=None): 419 420 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 421 422 return self._set_request(user, uid, recurrenceid, "requests") 423 424 def set_cancellation(self, user, uid, recurrenceid=None): 425 426 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 427 428 return self._set_request(user, uid, recurrenceid, "cancellations") 429 430 def queue_request(self, user, uid, recurrenceid=None): 431 432 """ 433 Queue a request for 'user' having the given 'uid'. If the optional 434 'recurrenceid' is specified, the request refers to a specific instance 435 or occurrence of an event. 436 """ 437 438 requests = self.get_requests(user) or [] 439 440 if (uid, recurrenceid) not in requests: 441 return self.set_request(user, uid, recurrenceid) 442 443 return False 444 445 def dequeue_request(self, user, uid, recurrenceid=None): 446 447 """ 448 Dequeue a request for 'user' having the given 'uid'. If the optional 449 'recurrenceid' is specified, the request refers to a specific instance 450 or occurrence of an event. 451 """ 452 453 requests = self.get_requests(user) or [] 454 455 try: 456 requests.remove((uid, recurrenceid)) 457 self.set_requests(user, requests) 458 except ValueError: 459 return False 460 else: 461 return True 462 463 def cancel_event(self, user, uid, recurrenceid=None): 464 465 """ 466 Queue an event for cancellation for 'user' having the given 'uid'. If 467 the optional 'recurrenceid' is specified, a specific instance or 468 occurrence of an event is cancelled. 469 """ 470 471 cancellations = self.get_cancellations(user) or [] 472 473 if (uid, recurrenceid) not in cancellations: 474 return self.set_cancellation(user, uid, recurrenceid) 475 476 return False 477 478 class FilePublisher(FileBase): 479 480 "A publisher of objects." 481 482 def __init__(self, store_dir=PUBLISH_DIR): 483 FileBase.__init__(self, store_dir) 484 485 def set_freebusy(self, user, freebusy): 486 487 "For the given 'user', set 'freebusy' details." 488 489 filename = self.get_object_in_store(user, "freebusy") 490 if not filename: 491 return False 492 493 record = [] 494 rwrite = record.append 495 496 rwrite(("ORGANIZER", {}, user)) 497 rwrite(("UID", {}, user)) 498 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 499 500 for start, end, uid, transp, recurrenceid in freebusy: 501 if not transp or transp == "OPAQUE": 502 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) 503 504 f = open(filename, "w") 505 try: 506 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 507 finally: 508 f.close() 509 fix_permissions(filename) 510 511 return True 512 513 # vim: tabstop=4 expandtab shiftwidth=4