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.dates import format_datetime, get_datetime 26 from imiptools.filesys import fix_permissions, FileBase 27 from imiptools.period import FreeBusyPeriod 28 from os.path import exists, isfile, join 29 from os import listdir, remove, rmdir 30 from time import sleep 31 import codecs 32 33 class FileStore(FileBase): 34 35 "A file store of tabular free/busy data and objects." 36 37 def __init__(self, store_dir=None): 38 FileBase.__init__(self, store_dir or STORE_DIR) 39 40 def acquire_lock(self, user, timeout=None): 41 FileBase.acquire_lock(self, timeout, user) 42 43 def release_lock(self, user): 44 FileBase.release_lock(self, user) 45 46 # Utility methods. 47 48 def _set_defaults(self, t, empty_defaults): 49 for i, default in empty_defaults: 50 if i >= len(t): 51 t += [None] * (i - len(t) + 1) 52 if not t[i]: 53 t[i] = default 54 return t 55 56 def _get_table(self, user, filename, empty_defaults=None): 57 58 """ 59 From the file for the given 'user' having the given 'filename', return 60 a list of tuples representing the file's contents. 61 62 The 'empty_defaults' is a list of (index, value) tuples indicating the 63 default value where a column either does not exist or provides an empty 64 value. 65 """ 66 67 self.acquire_lock(user) 68 try: 69 f = codecs.open(filename, "rb", encoding="utf-8") 70 try: 71 l = [] 72 for line in f.readlines(): 73 t = line.strip(" \r\n").split("\t") 74 if empty_defaults: 75 t = self._set_defaults(t, empty_defaults) 76 l.append(tuple(t)) 77 return l 78 finally: 79 f.close() 80 finally: 81 self.release_lock(user) 82 83 def _set_table(self, user, filename, items, empty_defaults=None): 84 85 """ 86 For the given 'user', write to the file having the given 'filename' the 87 'items'. 88 89 The 'empty_defaults' is a list of (index, value) tuples indicating the 90 default value where a column either does not exist or provides an empty 91 value. 92 """ 93 94 self.acquire_lock(user) 95 try: 96 f = codecs.open(filename, "wb", encoding="utf-8") 97 try: 98 for item in items: 99 if empty_defaults: 100 item = self._set_defaults(list(item), empty_defaults) 101 f.write("\t".join(item) + "\n") 102 finally: 103 f.close() 104 fix_permissions(filename) 105 finally: 106 self.release_lock(user) 107 108 # Store object access. 109 110 def _get_object(self, user, filename): 111 112 """ 113 Return the parsed object for the given 'user' having the given 114 'filename'. 115 """ 116 117 self.acquire_lock(user) 118 try: 119 f = open(filename, "rb") 120 try: 121 return parse_object(f, "utf-8") 122 finally: 123 f.close() 124 finally: 125 self.release_lock(user) 126 127 def _set_object(self, user, filename, node): 128 129 """ 130 Set an object for the given 'user' having the given 'filename', using 131 'node' to define the object. 132 """ 133 134 self.acquire_lock(user) 135 try: 136 f = open(filename, "wb") 137 try: 138 to_stream(f, node) 139 finally: 140 f.close() 141 fix_permissions(filename) 142 finally: 143 self.release_lock(user) 144 145 return True 146 147 def _remove_object(self, filename): 148 149 "Remove the object with the given 'filename'." 150 151 try: 152 remove(filename) 153 except OSError: 154 return False 155 156 return True 157 158 def _remove_collection(self, filename): 159 160 "Remove the collection with the given 'filename'." 161 162 try: 163 rmdir(filename) 164 except OSError: 165 return False 166 167 return True 168 169 # User discovery. 170 171 def get_users(self): 172 173 "Return a list of users." 174 175 return listdir(self.store_dir) 176 177 # Event and event metadata access. 178 179 def get_events(self, user): 180 181 "Return a list of event identifiers." 182 183 filename = self.get_object_in_store(user, "objects") 184 if not filename or not exists(filename): 185 return None 186 187 return [name for name in listdir(filename) if isfile(join(filename, name))] 188 189 def get_all_events(self, user): 190 191 "Return a set of (uid, recurrenceid) tuples for all events." 192 193 uids = self.get_events(user) 194 195 all_events = set() 196 for uid in uids: 197 all_events.add((uid, None)) 198 all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)]) 199 200 return all_events 201 202 def get_active_events(self, user): 203 204 "Return a set of uncancelled events of the form (uid, recurrenceid)." 205 206 all_events = self.get_all_events(user) 207 208 # Filter out cancelled events. 209 210 cancelled = self.get_cancellations(user) or [] 211 all_events.difference_update(cancelled) 212 return all_events 213 214 def get_event(self, user, uid, recurrenceid=None): 215 216 """ 217 Get the event for the given 'user' with the given 'uid'. If 218 the optional 'recurrenceid' is specified, a specific instance or 219 occurrence of an event is returned. 220 """ 221 222 if recurrenceid: 223 return self.get_recurrence(user, uid, recurrenceid) 224 else: 225 return self.get_complete_event(user, uid) 226 227 def get_complete_event(self, user, uid): 228 229 "Get the event for the given 'user' with the given 'uid'." 230 231 filename = self.get_object_in_store(user, "objects", uid) 232 if not filename or not exists(filename): 233 return None 234 235 return self._get_object(user, filename) 236 237 def set_event(self, user, uid, recurrenceid, node): 238 239 """ 240 Set an event for 'user' having the given 'uid' and 'recurrenceid' (which 241 if the latter is specified, a specific instance or occurrence of an 242 event is referenced), using the given 'node' description. 243 """ 244 245 if recurrenceid: 246 return self.set_recurrence(user, uid, recurrenceid, node) 247 else: 248 return self.set_complete_event(user, uid, node) 249 250 def set_complete_event(self, user, uid, node): 251 252 "Set an event for 'user' having the given 'uid' and 'node'." 253 254 filename = self.get_object_in_store(user, "objects", uid) 255 if not filename: 256 return False 257 258 return self._set_object(user, filename, node) 259 260 def remove_event(self, user, uid, recurrenceid=None): 261 262 """ 263 Remove an event for 'user' having the given 'uid'. If the optional 264 'recurrenceid' is specified, a specific instance or occurrence of an 265 event is removed. 266 """ 267 268 if recurrenceid: 269 return self.remove_recurrence(user, uid, recurrenceid) 270 else: 271 for recurrenceid in self.get_recurrences(user, uid) or []: 272 self.remove_recurrence(user, uid, recurrenceid) 273 return self.remove_complete_event(user, uid) 274 275 def remove_complete_event(self, user, uid): 276 277 "Remove an event for 'user' having the given 'uid'." 278 279 self.remove_recurrences(user, uid) 280 281 filename = self.get_object_in_store(user, "objects", uid) 282 if not filename: 283 return False 284 285 return self._remove_object(filename) 286 287 def get_recurrences(self, user, uid): 288 289 """ 290 Get additional event instances for an event of the given 'user' with the 291 indicated 'uid'. 292 """ 293 294 filename = self.get_object_in_store(user, "recurrences", uid) 295 if not filename or not exists(filename): 296 return [] 297 298 return [name for name in listdir(filename) if isfile(join(filename, name))] 299 300 def get_recurrence(self, user, uid, recurrenceid): 301 302 """ 303 For the event of the given 'user' with the given 'uid', return the 304 specific recurrence indicated by the 'recurrenceid'. 305 """ 306 307 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 308 if not filename or not exists(filename): 309 return None 310 311 return self._get_object(user, filename) 312 313 def set_recurrence(self, user, uid, recurrenceid, node): 314 315 "Set an event for 'user' having the given 'uid' and 'node'." 316 317 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 318 if not filename: 319 return False 320 321 return self._set_object(user, filename, node) 322 323 def remove_recurrence(self, user, uid, recurrenceid): 324 325 """ 326 Remove a special recurrence from an event stored by 'user' having the 327 given 'uid' and 'recurrenceid'. 328 """ 329 330 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 331 if not filename: 332 return False 333 334 return self._remove_object(filename) 335 336 def remove_recurrences(self, user, uid): 337 338 """ 339 Remove all recurrences for an event stored by 'user' having the given 340 'uid'. 341 """ 342 343 for recurrenceid in self.get_recurrences(user, uid): 344 self.remove_recurrence(user, uid, recurrenceid) 345 346 recurrences = self.get_object_in_store(user, "recurrences", uid) 347 if recurrences: 348 return self._remove_collection(recurrences) 349 350 return True 351 352 # Free/busy period providers, upon extension of the free/busy records. 353 354 def _get_freebusy_providers(self, user): 355 356 """ 357 Return the free/busy providers for the given 'user'. 358 359 This function returns any stored datetime and a list of providers as a 360 2-tuple. Each provider is itself a (uid, recurrenceid) tuple. 361 """ 362 363 filename = self.get_object_in_store(user, "freebusy-providers") 364 if not filename or not exists(filename): 365 return None 366 367 # Attempt to read providers, with a declaration of the datetime 368 # from which such providers are considered as still being active. 369 370 t = self._get_table(user, filename, [(1, None)]) 371 try: 372 dt_string = t[0][0] 373 except IndexError: 374 return None 375 376 return dt_string, t[1:] 377 378 def get_freebusy_providers(self, user, dt=None): 379 380 """ 381 Return a set of uncancelled events of the form (uid, recurrenceid) 382 providing free/busy details beyond the given datetime 'dt'. 383 384 If 'dt' is not specified, all events previously found to provide 385 details will be returned. Otherwise, if 'dt' is earlier than the 386 datetime recorded for the known providers, None is returned, indicating 387 that the list of providers must be recomputed. 388 389 This function returns a list of (uid, recurrenceid) tuples upon success. 390 """ 391 392 t = self._get_freebusy_providers(user) 393 if not t: 394 return None 395 396 dt_string, t = t 397 398 # If the requested datetime is earlier than the stated datetime, the 399 # providers will need to be recomputed. 400 401 if dt: 402 providers_dt = get_datetime(dt_string) 403 if not providers_dt or providers_dt > dt: 404 return None 405 406 # Otherwise, return the providers. 407 408 return t[1:] 409 410 def _set_freebusy_providers(self, user, dt_string, t): 411 412 "Set the given provider timestamp 'dt_string' and table 't'." 413 414 filename = self.get_object_in_store(user, "freebusy-providers") 415 if not filename: 416 return False 417 418 t.insert(0, (dt_string,)) 419 self._set_table(user, filename, t, [(1, "")]) 420 return True 421 422 def set_freebusy_providers(self, user, dt, providers): 423 424 """ 425 Define the uncancelled events providing free/busy details beyond the 426 given datetime 'dt'. 427 """ 428 429 t = [] 430 431 for obj in providers: 432 t.append((obj.get_uid(), obj.get_recurrenceid())) 433 434 return self._set_freebusy_providers(user, format_datetime(dt), t) 435 436 def append_freebusy_provider(self, user, provider): 437 438 "For the given 'user', append the free/busy 'provider'." 439 440 t = self._get_freebusy_providers(user) 441 if not t: 442 return False 443 444 dt_string, t = t 445 t.append((provider.get_uid(), provider.get_recurrenceid())) 446 447 return self._set_freebusy_providers(user, dt_string, t) 448 449 def remove_freebusy_provider(self, user, provider): 450 451 "For the given 'user', remove the free/busy 'provider'." 452 453 t = self._get_freebusy_providers(user) 454 if not t: 455 return False 456 457 dt_string, t = t 458 try: 459 t.remove((provider.get_uid(), provider.get_recurrenceid())) 460 except ValueError: 461 return False 462 463 return self._set_freebusy_providers(user, dt_string, t) 464 465 # Free/busy period access. 466 467 def get_freebusy(self, user): 468 469 "Get free/busy details for the given 'user'." 470 471 filename = self.get_object_in_store(user, "freebusy") 472 if not filename or not exists(filename): 473 return [] 474 else: 475 return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) 476 477 def get_freebusy_for_other(self, user, other): 478 479 "For the given 'user', get free/busy details for the 'other' user." 480 481 filename = self.get_object_in_store(user, "freebusy-other", other) 482 if not filename or not exists(filename): 483 return [] 484 else: 485 return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) 486 487 def set_freebusy(self, user, freebusy): 488 489 "For the given 'user', set 'freebusy' details." 490 491 filename = self.get_object_in_store(user, "freebusy") 492 if not filename: 493 return False 494 495 self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 496 return True 497 498 def set_freebusy_for_other(self, user, freebusy, other): 499 500 "For the given 'user', set 'freebusy' details for the 'other' user." 501 502 filename = self.get_object_in_store(user, "freebusy-other", other) 503 if not filename: 504 return False 505 506 self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 507 return True 508 509 # Object status details access. 510 511 def _get_requests(self, user, queue): 512 513 "Get requests for the given 'user' from the given 'queue'." 514 515 filename = self.get_object_in_store(user, queue) 516 if not filename or not exists(filename): 517 return None 518 519 return self._get_table(user, filename, [(1, None)]) 520 521 def get_requests(self, user): 522 523 "Get requests for the given 'user'." 524 525 return self._get_requests(user, "requests") 526 527 def get_cancellations(self, user): 528 529 "Get cancellations for the given 'user'." 530 531 return self._get_requests(user, "cancellations") 532 533 def _set_requests(self, user, requests, queue): 534 535 """ 536 For the given 'user', set the list of queued 'requests' in the given 537 'queue'. 538 """ 539 540 filename = self.get_object_in_store(user, queue) 541 if not filename: 542 return False 543 544 self.acquire_lock(user) 545 try: 546 f = open(filename, "w") 547 try: 548 for request in requests: 549 print >>f, "\t".join([value or "" for value in request]) 550 finally: 551 f.close() 552 fix_permissions(filename) 553 finally: 554 self.release_lock(user) 555 556 return True 557 558 def set_requests(self, user, requests): 559 560 "For the given 'user', set the list of queued 'requests'." 561 562 return self._set_requests(user, requests, "requests") 563 564 def set_cancellations(self, user, cancellations): 565 566 "For the given 'user', set the list of queued 'cancellations'." 567 568 return self._set_requests(user, cancellations, "cancellations") 569 570 def _set_request(self, user, uid, recurrenceid, queue): 571 572 """ 573 For the given 'user', set the queued 'uid' and 'recurrenceid' in the 574 given 'queue'. 575 """ 576 577 filename = self.get_object_in_store(user, queue) 578 if not filename: 579 return False 580 581 self.acquire_lock(user) 582 try: 583 f = open(filename, "a") 584 try: 585 print >>f, "\t".join([uid, recurrenceid or ""]) 586 finally: 587 f.close() 588 fix_permissions(filename) 589 finally: 590 self.release_lock(user) 591 592 return True 593 594 def set_request(self, user, uid, recurrenceid=None): 595 596 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 597 598 return self._set_request(user, uid, recurrenceid, "requests") 599 600 def set_cancellation(self, user, uid, recurrenceid=None): 601 602 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 603 604 return self._set_request(user, uid, recurrenceid, "cancellations") 605 606 def queue_request(self, user, uid, recurrenceid=None): 607 608 """ 609 Queue a request for 'user' having the given 'uid'. If the optional 610 'recurrenceid' is specified, the request refers to a specific instance 611 or occurrence of an event. 612 """ 613 614 requests = self.get_requests(user) or [] 615 616 if (uid, recurrenceid) not in requests: 617 return self.set_request(user, uid, recurrenceid) 618 619 return False 620 621 def dequeue_request(self, user, uid, recurrenceid=None): 622 623 """ 624 Dequeue a request for 'user' having the given 'uid'. If the optional 625 'recurrenceid' is specified, the request refers to a specific instance 626 or occurrence of an event. 627 """ 628 629 requests = self.get_requests(user) or [] 630 631 try: 632 requests.remove((uid, recurrenceid)) 633 self.set_requests(user, requests) 634 except ValueError: 635 return False 636 else: 637 return True 638 639 def cancel_event(self, user, uid, recurrenceid=None): 640 641 """ 642 Queue an event for cancellation for 'user' having the given 'uid'. If 643 the optional 'recurrenceid' is specified, a specific instance or 644 occurrence of an event is cancelled. 645 """ 646 647 cancellations = self.get_cancellations(user) or [] 648 649 if (uid, recurrenceid) not in cancellations: 650 return self.set_cancellation(user, uid, recurrenceid) 651 652 return False 653 654 class FilePublisher(FileBase): 655 656 "A publisher of objects." 657 658 def __init__(self, store_dir=None): 659 FileBase.__init__(self, store_dir or PUBLISH_DIR) 660 661 def set_freebusy(self, user, freebusy): 662 663 "For the given 'user', set 'freebusy' details." 664 665 filename = self.get_object_in_store(user, "freebusy") 666 if not filename: 667 return False 668 669 record = [] 670 rwrite = record.append 671 672 rwrite(("ORGANIZER", {}, user)) 673 rwrite(("UID", {}, user)) 674 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 675 676 for fb in freebusy: 677 if not fb.transp or fb.transp == "OPAQUE": 678 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 679 map(format_datetime, [fb.get_start_point(), fb.get_end_point()])))) 680 681 f = open(filename, "wb") 682 try: 683 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 684 finally: 685 f.close() 686 fix_permissions(filename) 687 688 return True 689 690 # vim: tabstop=4 expandtab shiftwidth=4