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