1 #!/usr/bin/env python 2 3 """ 4 A simple filesystem-based store of calendar data. 5 6 Copyright (C) 2014, 2015, 2016 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, JOURNAL_DIR 24 from imiptools.data import make_calendar, parse_object, to_stream 25 from imiptools.dates import format_datetime, get_datetime, to_timezone 26 from imiptools.filesys import fix_permissions, FileBase 27 from imiptools.period import FreeBusyPeriod 28 from os.path import isdir, isfile, join 29 from os import listdir, remove, rmdir 30 from time import sleep 31 import codecs 32 33 class FileStoreBase(FileBase): 34 35 "A file store supporting user-specific locking and tabular data." 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 # Utility methods. 44 45 def _set_defaults(self, t, empty_defaults): 46 for i, default in empty_defaults: 47 if i >= len(t): 48 t += [None] * (i - len(t) + 1) 49 if not t[i]: 50 t[i] = default 51 return t 52 53 def _get_table(self, user, filename, empty_defaults=None): 54 55 """ 56 From the file for the given 'user' having the given 'filename', return 57 a list of tuples representing the file's contents. 58 59 The 'empty_defaults' is a list of (index, value) tuples indicating the 60 default value where a column either does not exist or provides an empty 61 value. 62 """ 63 64 f = codecs.open(filename, "rb", encoding="utf-8") 65 try: 66 l = [] 67 for line in f.readlines(): 68 t = line.strip(" \r\n").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 76 def _get_table_atomic(self, user, filename, empty_defaults=None): 77 78 """ 79 From the file for the given 'user' having the given 'filename', return 80 a list of tuples representing the file's contents. 81 82 The 'empty_defaults' is a list of (index, value) tuples indicating the 83 default value where a column either does not exist or provides an empty 84 value. 85 """ 86 87 self.acquire_lock(user) 88 try: 89 return self._get_table(user, filename, empty_defaults) 90 finally: 91 self.release_lock(user) 92 93 def _set_table(self, user, filename, items, empty_defaults=None): 94 95 """ 96 For the given 'user', write to the file having the given 'filename' the 97 'items'. 98 99 The 'empty_defaults' is a list of (index, value) tuples indicating the 100 default value where a column either does not exist or provides an empty 101 value. 102 """ 103 104 f = codecs.open(filename, "wb", encoding="utf-8") 105 try: 106 for item in items: 107 self._set_table_item(f, item, empty_defaults) 108 finally: 109 f.close() 110 fix_permissions(filename) 111 112 def _set_table_item(self, f, item, empty_defaults=None): 113 114 "Set in table 'f' the given 'item', using any 'empty_defaults'." 115 116 if empty_defaults: 117 item = self._set_defaults(list(item), empty_defaults) 118 f.write("\t".join(item) + "\n") 119 120 def _set_table_atomic(self, user, filename, items, empty_defaults=None): 121 122 """ 123 For the given 'user', write to the file having the given 'filename' the 124 'items'. 125 126 The 'empty_defaults' is a list of (index, value) tuples indicating the 127 default value where a column either does not exist or provides an empty 128 value. 129 """ 130 131 self.acquire_lock(user) 132 try: 133 self._set_table(user, filename, items, empty_defaults) 134 finally: 135 self.release_lock(user) 136 137 class FileStore(FileStoreBase): 138 139 "A file store of tabular free/busy data and objects." 140 141 def __init__(self, store_dir=None): 142 FileBase.__init__(self, store_dir or STORE_DIR) 143 144 # Store object access. 145 146 def _get_object(self, user, filename): 147 148 """ 149 Return the parsed object for the given 'user' having the given 150 'filename'. 151 """ 152 153 self.acquire_lock(user) 154 try: 155 f = open(filename, "rb") 156 try: 157 return parse_object(f, "utf-8") 158 finally: 159 f.close() 160 finally: 161 self.release_lock(user) 162 163 def _set_object(self, user, filename, node): 164 165 """ 166 Set an object for the given 'user' having the given 'filename', using 167 'node' to define the object. 168 """ 169 170 self.acquire_lock(user) 171 try: 172 f = open(filename, "wb") 173 try: 174 to_stream(f, node) 175 finally: 176 f.close() 177 fix_permissions(filename) 178 finally: 179 self.release_lock(user) 180 181 return True 182 183 def _remove_object(self, filename): 184 185 "Remove the object with the given 'filename'." 186 187 try: 188 remove(filename) 189 except OSError: 190 return False 191 192 return True 193 194 def _remove_collection(self, filename): 195 196 "Remove the collection with the given 'filename'." 197 198 try: 199 rmdir(filename) 200 except OSError: 201 return False 202 203 return True 204 205 # User discovery. 206 207 def get_users(self): 208 209 "Return a list of users." 210 211 return listdir(self.store_dir) 212 213 # Event and event metadata access. 214 215 def get_events(self, user): 216 217 "Return a list of event identifiers." 218 219 filename = self.get_object_in_store(user, "objects") 220 if not filename or not isdir(filename): 221 return None 222 223 return [name for name in listdir(filename) if isfile(join(filename, name))] 224 225 def get_all_events(self, user): 226 227 "Return a set of (uid, recurrenceid) tuples for all events." 228 229 uids = self.get_events(user) 230 if not uids: 231 return set() 232 233 all_events = set() 234 for uid in uids: 235 all_events.add((uid, None)) 236 all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)]) 237 238 return all_events 239 240 def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None): 241 242 """ 243 Get the filename providing the event for the given 'user' with the given 244 'uid'. If the optional 'recurrenceid' is specified, a specific instance 245 or occurrence of an event is returned. 246 247 Where 'dirname' is specified, the given directory name is used as the 248 base of the location within which any filename will reside. 249 """ 250 251 if recurrenceid: 252 return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username) 253 else: 254 return self.get_complete_event_filename(user, uid, dirname, username) 255 256 def get_event(self, user, uid, recurrenceid=None, dirname=None): 257 258 """ 259 Get the event for the given 'user' with the given 'uid'. If 260 the optional 'recurrenceid' is specified, a specific instance or 261 occurrence of an event is returned. 262 """ 263 264 filename = self.get_event_filename(user, uid, recurrenceid, dirname) 265 if not filename or not isfile(filename): 266 return None 267 268 return filename and self._get_object(user, filename) 269 270 def get_complete_event_filename(self, user, uid, dirname=None, username=None): 271 272 """ 273 Get the filename providing the event for the given 'user' with the given 274 'uid'. 275 276 Where 'dirname' is specified, the given directory name is used as the 277 base of the location within which any filename will reside. 278 279 Where 'username' is specified, the event details will reside in a file 280 bearing that name within a directory having 'uid' as its name. 281 """ 282 283 return self.get_object_in_store(user, dirname, "objects", uid, username) 284 285 def get_complete_event(self, user, uid): 286 287 "Get the event for the given 'user' with the given 'uid'." 288 289 filename = self.get_complete_event_filename(user, uid) 290 if not filename or not isfile(filename): 291 return None 292 293 return filename and self._get_object(user, filename) 294 295 def set_event(self, user, uid, recurrenceid, node): 296 297 """ 298 Set an event for 'user' having the given 'uid' and 'recurrenceid' (which 299 if the latter is specified, a specific instance or occurrence of an 300 event is referenced), using the given 'node' description. 301 """ 302 303 if recurrenceid: 304 return self.set_recurrence(user, uid, recurrenceid, node) 305 else: 306 return self.set_complete_event(user, uid, node) 307 308 def set_complete_event(self, user, uid, node): 309 310 "Set an event for 'user' having the given 'uid' and 'node'." 311 312 filename = self.get_object_in_store(user, "objects", uid) 313 if not filename: 314 return False 315 316 return self._set_object(user, filename, node) 317 318 def remove_event(self, user, uid, recurrenceid=None): 319 320 """ 321 Remove an event for 'user' having the given 'uid'. If the optional 322 'recurrenceid' is specified, a specific instance or occurrence of an 323 event is removed. 324 """ 325 326 if recurrenceid: 327 return self.remove_recurrence(user, uid, recurrenceid) 328 else: 329 for recurrenceid in self.get_recurrences(user, uid) or []: 330 self.remove_recurrence(user, uid, recurrenceid) 331 return self.remove_complete_event(user, uid) 332 333 def remove_complete_event(self, user, uid): 334 335 "Remove an event for 'user' having the given 'uid'." 336 337 self.remove_recurrences(user, uid) 338 339 filename = self.get_object_in_store(user, "objects", uid) 340 if not filename: 341 return False 342 343 return self._remove_object(filename) 344 345 def get_recurrences(self, user, uid): 346 347 """ 348 Get additional event instances for an event of the given 'user' with the 349 indicated 'uid'. Both active and cancelled recurrences are returned. 350 """ 351 352 return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid) 353 354 def get_active_recurrences(self, user, uid): 355 356 """ 357 Get additional event instances for an event of the given 'user' with the 358 indicated 'uid'. Cancelled recurrences are not returned. 359 """ 360 361 filename = self.get_object_in_store(user, "recurrences", uid) 362 if not filename or not isdir(filename): 363 return [] 364 365 return [name for name in listdir(filename) if isfile(join(filename, name))] 366 367 def get_cancelled_recurrences(self, user, uid): 368 369 """ 370 Get additional event instances for an event of the given 'user' with the 371 indicated 'uid'. Only cancelled recurrences are returned. 372 """ 373 374 filename = self.get_object_in_store(user, "cancellations", "recurrences", uid) 375 if not filename or not isdir(filename): 376 return [] 377 378 return [name for name in listdir(filename) if isfile(join(filename, name))] 379 380 def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None): 381 382 """ 383 For the event of the given 'user' with the given 'uid', return the 384 filename providing the recurrence with the given 'recurrenceid'. 385 386 Where 'dirname' is specified, the given directory name is used as the 387 base of the location within which any filename will reside. 388 389 Where 'username' is specified, the event details will reside in a file 390 bearing that name within a directory having 'uid' as its name. 391 """ 392 393 return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username) 394 395 def get_recurrence(self, user, uid, recurrenceid): 396 397 """ 398 For the event of the given 'user' with the given 'uid', return the 399 specific recurrence indicated by the 'recurrenceid'. 400 """ 401 402 filename = self.get_recurrence_filename(user, uid, recurrenceid) 403 if not filename or not isfile(filename): 404 return None 405 406 return filename and self._get_object(user, filename) 407 408 def set_recurrence(self, user, uid, recurrenceid, node): 409 410 "Set an event for 'user' having the given 'uid' and 'node'." 411 412 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 413 if not filename: 414 return False 415 416 return self._set_object(user, filename, node) 417 418 def remove_recurrence(self, user, uid, recurrenceid): 419 420 """ 421 Remove a special recurrence from an event stored by 'user' having the 422 given 'uid' and 'recurrenceid'. 423 """ 424 425 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 426 if not filename: 427 return False 428 429 return self._remove_object(filename) 430 431 def remove_recurrences(self, user, uid): 432 433 """ 434 Remove all recurrences for an event stored by 'user' having the given 435 'uid'. 436 """ 437 438 for recurrenceid in self.get_recurrences(user, uid): 439 self.remove_recurrence(user, uid, recurrenceid) 440 441 recurrences = self.get_object_in_store(user, "recurrences", uid) 442 if recurrences: 443 return self._remove_collection(recurrences) 444 445 return True 446 447 # Free/busy period providers, upon extension of the free/busy records. 448 449 def _get_freebusy_providers(self, user): 450 451 """ 452 Return the free/busy providers for the given 'user'. 453 454 This function returns any stored datetime and a list of providers as a 455 2-tuple. Each provider is itself a (uid, recurrenceid) tuple. 456 """ 457 458 filename = self.get_object_in_store(user, "freebusy-providers") 459 if not filename or not isfile(filename): 460 return None 461 462 # Attempt to read providers, with a declaration of the datetime 463 # from which such providers are considered as still being active. 464 465 t = self._get_table_atomic(user, filename, [(1, None)]) 466 try: 467 dt_string = t[0][0] 468 except IndexError: 469 return None 470 471 return dt_string, t[1:] 472 473 def get_freebusy_providers(self, user, dt=None): 474 475 """ 476 Return a set of uncancelled events of the form (uid, recurrenceid) 477 providing free/busy details beyond the given datetime 'dt'. 478 479 If 'dt' is not specified, all events previously found to provide 480 details will be returned. Otherwise, if 'dt' is earlier than the 481 datetime recorded for the known providers, None is returned, indicating 482 that the list of providers must be recomputed. 483 484 This function returns a list of (uid, recurrenceid) tuples upon success. 485 """ 486 487 t = self._get_freebusy_providers(user) 488 if not t: 489 return None 490 491 dt_string, t = t 492 493 # If the requested datetime is earlier than the stated datetime, the 494 # providers will need to be recomputed. 495 496 if dt: 497 providers_dt = get_datetime(dt_string) 498 if not providers_dt or providers_dt > dt: 499 return None 500 501 # Otherwise, return the providers. 502 503 return t[1:] 504 505 def _set_freebusy_providers(self, user, dt_string, t): 506 507 "Set the given provider timestamp 'dt_string' and table 't'." 508 509 filename = self.get_object_in_store(user, "freebusy-providers") 510 if not filename: 511 return False 512 513 t.insert(0, (dt_string,)) 514 self._set_table_atomic(user, filename, t, [(1, "")]) 515 return True 516 517 def set_freebusy_providers(self, user, dt, providers): 518 519 """ 520 Define the uncancelled events providing free/busy details beyond the 521 given datetime 'dt'. 522 """ 523 524 t = [] 525 526 for obj in providers: 527 t.append((obj.get_uid(), obj.get_recurrenceid())) 528 529 return self._set_freebusy_providers(user, format_datetime(dt), t) 530 531 def append_freebusy_provider(self, user, provider): 532 533 "For the given 'user', append the free/busy 'provider'." 534 535 t = self._get_freebusy_providers(user) 536 if not t: 537 return False 538 539 dt_string, t = t 540 t.append((provider.get_uid(), provider.get_recurrenceid())) 541 542 return self._set_freebusy_providers(user, dt_string, t) 543 544 def remove_freebusy_provider(self, user, provider): 545 546 "For the given 'user', remove the free/busy 'provider'." 547 548 t = self._get_freebusy_providers(user) 549 if not t: 550 return False 551 552 dt_string, t = t 553 try: 554 t.remove((provider.get_uid(), provider.get_recurrenceid())) 555 except ValueError: 556 return False 557 558 return self._set_freebusy_providers(user, dt_string, t) 559 560 # Free/busy period access. 561 562 def get_freebusy(self, user, name=None, get_table=None): 563 564 "Get free/busy details for the given 'user'." 565 566 filename = self.get_object_in_store(user, name or "freebusy") 567 if not filename or not isfile(filename): 568 return [] 569 else: 570 return map(lambda t: FreeBusyPeriod(*t), 571 (get_table or self._get_table_atomic)(user, filename, [(4, None)])) 572 573 def get_freebusy_for_other(self, user, other, get_table=None): 574 575 "For the given 'user', get free/busy details for the 'other' user." 576 577 filename = self.get_object_in_store(user, "freebusy-other", other) 578 if not filename or not isfile(filename): 579 return [] 580 else: 581 return map(lambda t: FreeBusyPeriod(*t), 582 (get_table or self._get_table_atomic)(user, filename, [(4, None)])) 583 584 def set_freebusy(self, user, freebusy, name=None, set_table=None): 585 586 "For the given 'user', set 'freebusy' details." 587 588 filename = self.get_object_in_store(user, name or "freebusy") 589 if not filename: 590 return False 591 592 (set_table or self._set_table_atomic)(user, filename, 593 map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 594 return True 595 596 def set_freebusy_for_other(self, user, freebusy, other, set_table=None): 597 598 "For the given 'user', set 'freebusy' details for the 'other' user." 599 600 filename = self.get_object_in_store(user, "freebusy-other", other) 601 if not filename: 602 return False 603 604 (set_table or self._set_table_atomic)(user, filename, 605 map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 606 return True 607 608 # Tentative free/busy periods related to countering. 609 610 def get_freebusy_offers(self, user): 611 612 "Get free/busy offers for the given 'user'." 613 614 offers = [] 615 expired = [] 616 now = to_timezone(datetime.utcnow(), "UTC") 617 618 # Expire old offers and save the collection if modified. 619 620 self.acquire_lock(user) 621 try: 622 l = self.get_freebusy(user, "freebusy-offers") 623 for fb in l: 624 if fb.expires and get_datetime(fb.expires) <= now: 625 expired.append(fb) 626 else: 627 offers.append(fb) 628 629 if expired: 630 self.set_freebusy_offers(user, offers) 631 finally: 632 self.release_lock(user) 633 634 return offers 635 636 def set_freebusy_offers(self, user, freebusy): 637 638 "For the given 'user', set 'freebusy' offers." 639 640 return self.set_freebusy(user, freebusy, "freebusy-offers") 641 642 # Requests and counter-proposals. 643 644 def _get_requests(self, user, queue): 645 646 "Get requests for the given 'user' from the given 'queue'." 647 648 filename = self.get_object_in_store(user, queue) 649 if not filename or not isfile(filename): 650 return None 651 652 return self._get_table_atomic(user, filename, [(1, None), (2, None)]) 653 654 def get_requests(self, user): 655 656 "Get requests for the given 'user'." 657 658 return self._get_requests(user, "requests") 659 660 def _set_requests(self, user, requests, queue): 661 662 """ 663 For the given 'user', set the list of queued 'requests' in the given 664 'queue'. 665 """ 666 667 filename = self.get_object_in_store(user, queue) 668 if not filename: 669 return False 670 671 self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")]) 672 return True 673 674 def set_requests(self, user, requests): 675 676 "For the given 'user', set the list of queued 'requests'." 677 678 return self._set_requests(user, requests, "requests") 679 680 def _set_request(self, user, request, queue): 681 682 """ 683 For the given 'user', set the given 'request' in the given 'queue'. 684 """ 685 686 filename = self.get_object_in_store(user, queue) 687 if not filename: 688 return False 689 690 self.acquire_lock(user) 691 try: 692 f = codecs.open(filename, "ab", encoding="utf-8") 693 try: 694 self._set_table_item(f, request, [(1, ""), (2, "")]) 695 finally: 696 f.close() 697 fix_permissions(filename) 698 finally: 699 self.release_lock(user) 700 701 return True 702 703 def set_request(self, user, uid, recurrenceid=None, type=None): 704 705 """ 706 For the given 'user', set the queued 'uid' and 'recurrenceid', 707 indicating a request, along with any given 'type'. 708 """ 709 710 return self._set_request(user, (uid, recurrenceid, type), "requests") 711 712 def queue_request(self, user, uid, recurrenceid=None, type=None): 713 714 """ 715 Queue a request for 'user' having the given 'uid'. If the optional 716 'recurrenceid' is specified, the entry refers to a specific instance 717 or occurrence of an event. The 'type' parameter can be used to indicate 718 a specific type of request. 719 """ 720 721 requests = self.get_requests(user) or [] 722 723 if not self.have_request(requests, uid, recurrenceid): 724 return self.set_request(user, uid, recurrenceid, type) 725 726 return False 727 728 def dequeue_request(self, user, uid, recurrenceid=None): 729 730 """ 731 Dequeue all requests for 'user' having the given 'uid'. If the optional 732 'recurrenceid' is specified, all requests for that specific instance or 733 occurrence of an event are dequeued. 734 """ 735 736 requests = self.get_requests(user) or [] 737 result = [] 738 739 for request in requests: 740 if request[:2] != (uid, recurrenceid): 741 result.append(request) 742 743 self.set_requests(user, result) 744 return True 745 746 def has_request(self, user, uid, recurrenceid=None, type=None, strict=False): 747 return self.have_request(self.get_requests(user) or [], uid, recurrenceid, type, strict) 748 749 def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False): 750 751 """ 752 Return whether 'requests' contains a request with the given 'uid' and 753 any specified 'recurrenceid' and 'type'. If 'strict' is set to a true 754 value, the precise type of the request must match; otherwise, any type 755 of request for the identified object may be matched. 756 """ 757 758 for request in requests: 759 if request[:2] == (uid, recurrenceid) and ( 760 not strict or 761 not request[2:] and not type or 762 request[2:] and request[2] == type): 763 764 return True 765 766 return False 767 768 def get_counters(self, user, uid, recurrenceid=None): 769 770 """ 771 For the given 'user', return a list of users from whom counter-proposals 772 have been received for the given 'uid' and optional 'recurrenceid'. 773 """ 774 775 filename = self.get_event_filename(user, uid, recurrenceid, "counters") 776 if not filename or not isdir(filename): 777 return False 778 779 return [name for name in listdir(filename) if isfile(join(filename, name))] 780 781 def get_counter(self, user, other, uid, recurrenceid=None): 782 783 """ 784 For the given 'user', return the counter-proposal from 'other' for the 785 given 'uid' and optional 'recurrenceid'. 786 """ 787 788 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 789 if not filename: 790 return False 791 792 return self._get_object(user, filename) 793 794 def set_counter(self, user, other, node, uid, recurrenceid=None): 795 796 """ 797 For the given 'user', store a counter-proposal received from 'other' the 798 given 'node' representing that proposal for the given 'uid' and 799 'recurrenceid'. 800 """ 801 802 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 803 if not filename: 804 return False 805 806 return self._set_object(user, filename, node) 807 808 def remove_counters(self, user, uid, recurrenceid=None): 809 810 """ 811 For the given 'user', remove all counter-proposals associated with the 812 given 'uid' and 'recurrenceid'. 813 """ 814 815 filename = self.get_event_filename(user, uid, recurrenceid, "counters") 816 if not filename or not isdir(filename): 817 return False 818 819 removed = False 820 821 for other in listdir(filename): 822 counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 823 removed = removed or self._remove_object(counter_filename) 824 825 return removed 826 827 def remove_counter(self, user, other, uid, recurrenceid=None): 828 829 """ 830 For the given 'user', remove any counter-proposal from 'other' 831 associated with the given 'uid' and 'recurrenceid'. 832 """ 833 834 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 835 if not filename or not isfile(filename): 836 return False 837 838 return self._remove_object(filename) 839 840 # Event cancellation. 841 842 def cancel_event(self, user, uid, recurrenceid=None): 843 844 """ 845 Cancel an event for 'user' having the given 'uid'. If the optional 846 'recurrenceid' is specified, a specific instance or occurrence of an 847 event is cancelled. 848 """ 849 850 filename = self.get_event_filename(user, uid, recurrenceid) 851 cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 852 853 if filename and cancelled_filename and isfile(filename): 854 return self.move_object(filename, cancelled_filename) 855 856 return False 857 858 def uncancel_event(self, user, uid, recurrenceid=None): 859 860 """ 861 Uncancel an event for 'user' having the given 'uid'. If the optional 862 'recurrenceid' is specified, a specific instance or occurrence of an 863 event is uncancelled. 864 """ 865 866 filename = self.get_event_filename(user, uid, recurrenceid) 867 cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 868 869 if filename and cancelled_filename and isfile(cancelled_filename): 870 return self.move_object(cancelled_filename, filename) 871 872 return False 873 874 def remove_cancellations(self, user, uid, recurrenceid=None): 875 876 """ 877 Remove cancellations for 'user' for any event having the given 'uid'. If 878 the optional 'recurrenceid' is specified, a specific instance or 879 occurrence of an event is affected. 880 """ 881 882 # Remove all recurrence cancellations if a general event is indicated. 883 884 if not recurrenceid: 885 for _recurrenceid in self.get_cancelled_recurrences(user, uid): 886 self.remove_cancellation(user, uid, _recurrenceid) 887 888 return self.remove_cancellation(user, uid, recurrenceid) 889 890 def remove_cancellation(self, user, uid, recurrenceid=None): 891 892 """ 893 Remove a cancellation for 'user' for the event having the given 'uid'. 894 If the optional 'recurrenceid' is specified, a specific instance or 895 occurrence of an event is affected. 896 """ 897 898 # Remove any parent event cancellation or a specific recurrence 899 # cancellation if indicated. 900 901 filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 902 903 if filename and isfile(filename): 904 return self._remove_object(filename) 905 906 return False 907 908 class FilePublisher(FileBase): 909 910 "A publisher of objects." 911 912 def __init__(self, store_dir=None): 913 FileBase.__init__(self, store_dir or PUBLISH_DIR) 914 915 def set_freebusy(self, user, freebusy): 916 917 "For the given 'user', set 'freebusy' details." 918 919 filename = self.get_object_in_store(user, "freebusy") 920 if not filename: 921 return False 922 923 record = [] 924 rwrite = record.append 925 926 rwrite(("ORGANIZER", {}, user)) 927 rwrite(("UID", {}, user)) 928 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 929 930 for fb in freebusy: 931 if not fb.transp or fb.transp == "OPAQUE": 932 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 933 map(format_datetime, [fb.get_start_point(), fb.get_end_point()])))) 934 935 f = open(filename, "wb") 936 try: 937 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 938 finally: 939 f.close() 940 fix_permissions(filename) 941 942 return True 943 944 class FileJournal(FileStoreBase): 945 946 "A journal system to support quotas." 947 948 def __init__(self, store_dir=None): 949 FileBase.__init__(self, store_dir or JOURNAL_DIR) 950 951 # Groups of users sharing quotas. 952 953 def get_groups(self, quota): 954 955 "Return the identity mappings for the given 'quota' as a dictionary." 956 957 filename = self.get_object_in_store(quota, "groups") 958 if not filename or not isfile(filename): 959 return {} 960 961 return dict(self._get_table_atomic(quota, filename)) 962 963 def get_limits(self, quota): 964 965 """ 966 Return the limits for the 'quota' as a dictionary mapping identities or 967 groups to durations. 968 """ 969 970 filename = self.get_object_in_store(quota, "limits") 971 if not filename or not isfile(filename): 972 return None 973 974 return dict(self._get_table_atomic(quota, filename)) 975 976 # Free/busy period access. 977 978 def get_freebusy(self, quota, user, get_table=None): 979 980 "Get free/busy details for the given 'quota' and 'user'." 981 982 filename = self.get_object_in_store(quota, "freebusy", user) 983 if not filename or not isfile(filename): 984 return [] 985 else: 986 return map(lambda t: FreeBusyPeriod(*t), 987 (get_table or self._get_table_atomic)(quota, filename, [(4, None)])) 988 989 def set_freebusy(self, quota, user, freebusy, set_table=None): 990 991 "For the given 'quota' and 'user', set 'freebusy' details." 992 993 filename = self.get_object_in_store(quota, "freebusy", user) 994 if not filename: 995 return False 996 997 (set_table or self._set_table_atomic)(quota, filename, 998 map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 999 return True 1000 1001 # Journal entry methods. 1002 1003 def get_entries(self, quota, group): 1004 1005 """ 1006 Return a list of journal entries for the given 'quota' for the indicated 1007 'group'. 1008 """ 1009 1010 filename = self.get_object_in_store(quota, "journal", group) 1011 if not filename or not isfile(filename): 1012 return [] 1013 1014 return self._get_table_atomic(quota, filename, [(1, None)]) 1015 1016 def set_entries(self, quota, group, entries): 1017 1018 """ 1019 For the given 'quota' and indicated 'group', set the list of journal 1020 'entries'. 1021 """ 1022 1023 filename = self.get_object_in_store(quota, "journal", group) 1024 if not filename: 1025 return False 1026 1027 self._set_table_atomic(quota, filename, entries, [(1, "")]) 1028 return True 1029 1030 # vim: tabstop=4 expandtab shiftwidth=4