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