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, 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 Store(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 [] 236 237 return [name for name in listdir(filename) if isfile(join(filename, name))] 238 239 def get_cancelled_events(self, user): 240 241 "Return a list of event identifiers for cancelled events." 242 243 filename = self.get_object_in_store(user, "cancellations", "objects") 244 if not filename or not isdir(filename): 245 return [] 246 247 return [name for name in listdir(filename) if isfile(join(filename, name))] 248 249 def get_event(self, user, uid, recurrenceid=None, dirname=None): 250 251 """ 252 Get the event for the given 'user' with the given 'uid'. If 253 the optional 'recurrenceid' is specified, a specific instance or 254 occurrence of an event is returned. 255 """ 256 257 filename = self.get_event_filename(user, uid, recurrenceid, dirname) 258 if not filename or not isfile(filename): 259 return None 260 261 return filename and self._get_object(user, filename) 262 263 def get_complete_event(self, user, uid): 264 265 "Get the event for the given 'user' with the given 'uid'." 266 267 filename = self.get_complete_event_filename(user, uid) 268 if not filename or not isfile(filename): 269 return None 270 271 return filename and self._get_object(user, filename) 272 273 def set_complete_event(self, user, uid, node): 274 275 "Set an event for 'user' having the given 'uid' and 'node'." 276 277 filename = self.get_object_in_store(user, "objects", uid) 278 if not filename: 279 return False 280 281 return self._set_object(user, filename, node) 282 283 def remove_parent_event(self, user, uid): 284 285 "Remove the parent event for 'user' having the given 'uid'." 286 287 filename = self.get_object_in_store(user, "objects", uid) 288 if not filename: 289 return False 290 291 return self._remove_object(filename) 292 293 def get_recurrences(self, user, uid): 294 295 """ 296 Get additional event instances for an event of the given 'user' with the 297 indicated 'uid'. Both active and cancelled recurrences are returned. 298 """ 299 300 return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid) 301 302 def get_active_recurrences(self, user, uid): 303 304 """ 305 Get additional event instances for an event of the given 'user' with the 306 indicated 'uid'. Cancelled recurrences are not returned. 307 """ 308 309 filename = self.get_object_in_store(user, "recurrences", uid) 310 if not filename or not isdir(filename): 311 return [] 312 313 return [name for name in listdir(filename) if isfile(join(filename, name))] 314 315 def get_cancelled_recurrences(self, user, uid): 316 317 """ 318 Get additional event instances for an event of the given 'user' with the 319 indicated 'uid'. Only cancelled recurrences are returned. 320 """ 321 322 filename = self.get_object_in_store(user, "cancellations", "recurrences", uid) 323 if not filename or not isdir(filename): 324 return [] 325 326 return [name for name in listdir(filename) if isfile(join(filename, name))] 327 328 def get_recurrence(self, user, uid, recurrenceid): 329 330 """ 331 For the event of the given 'user' with the given 'uid', return the 332 specific recurrence indicated by the 'recurrenceid'. 333 """ 334 335 filename = self.get_recurrence_filename(user, uid, recurrenceid) 336 if not filename or not isfile(filename): 337 return None 338 339 return filename and self._get_object(user, filename) 340 341 def set_recurrence(self, user, uid, recurrenceid, node): 342 343 "Set an event for 'user' having the given 'uid' and 'node'." 344 345 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 346 if not filename: 347 return False 348 349 return self._set_object(user, filename, node) 350 351 def remove_recurrence(self, user, uid, recurrenceid): 352 353 """ 354 Remove a special recurrence from an event stored by 'user' having the 355 given 'uid' and 'recurrenceid'. 356 """ 357 358 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 359 if not filename: 360 return False 361 362 return self._remove_object(filename) 363 364 def remove_recurrence_collection(self, user, uid): 365 366 """ 367 Remove the collection of recurrences stored by 'user' having the given 368 'uid'. 369 """ 370 371 recurrences = self.get_object_in_store(user, "recurrences", uid) 372 if recurrences: 373 return self._remove_collection(recurrences) 374 375 return True 376 377 # Event filename computation. 378 379 def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None): 380 381 """ 382 Get the filename providing the event for the given 'user' with the given 383 'uid'. If the optional 'recurrenceid' is specified, a specific instance 384 or occurrence of an event is returned. 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 390 if recurrenceid: 391 return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username) 392 else: 393 return self.get_complete_event_filename(user, uid, dirname, username) 394 395 def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None): 396 397 """ 398 For the event of the given 'user' with the given 'uid', return the 399 filename providing the recurrence with the given 'recurrenceid'. 400 401 Where 'dirname' is specified, the given directory name is used as the 402 base of the location within which any filename will reside. 403 404 Where 'username' is specified, the event details will reside in a file 405 bearing that name within a directory having 'uid' as its name. 406 """ 407 408 return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username) 409 410 def get_complete_event_filename(self, user, uid, dirname=None, username=None): 411 412 """ 413 Get the filename providing the event for the given 'user' with the given 414 'uid'. 415 416 Where 'dirname' is specified, the given directory name is used as the 417 base of the location within which any filename will reside. 418 419 Where 'username' is specified, the event details will reside in a file 420 bearing that name within a directory having 'uid' as its name. 421 """ 422 423 return self.get_object_in_store(user, dirname, "objects", uid, username) 424 425 # Free/busy period providers, upon extension of the free/busy records. 426 427 def _get_freebusy_providers(self, user): 428 429 """ 430 Return the free/busy providers for the given 'user'. 431 432 This function returns any stored datetime and a list of providers as a 433 2-tuple. Each provider is itself a (uid, recurrenceid) tuple. 434 """ 435 436 filename = self.get_object_in_store(user, "freebusy-providers") 437 if not filename or not isfile(filename): 438 return None 439 440 # Attempt to read providers, with a declaration of the datetime 441 # from which such providers are considered as still being active. 442 443 t = self._get_table_atomic(user, filename, [(1, None)]) 444 try: 445 dt_string = t[0][0] 446 except IndexError: 447 return None 448 449 return dt_string, t[1:] 450 451 def _set_freebusy_providers(self, user, dt_string, t): 452 453 "Set the given provider timestamp 'dt_string' and table 't'." 454 455 filename = self.get_object_in_store(user, "freebusy-providers") 456 if not filename: 457 return False 458 459 t.insert(0, (dt_string,)) 460 self._set_table_atomic(user, filename, t, [(1, "")]) 461 return True 462 463 # Free/busy period access. 464 465 def get_freebusy(self, user, name=None, mutable=False): 466 467 "Get free/busy details for the given 'user'." 468 469 filename = self.get_object_in_store(user, name or "freebusy") 470 471 if not filename or not isfile(filename): 472 periods = [] 473 else: 474 periods = map(lambda t: FreeBusyPeriod(*t), 475 self._get_table_atomic(user, filename)) 476 477 return FreeBusyCollection(periods, mutable) 478 479 def get_freebusy_for_other(self, user, other, mutable=False): 480 481 "For the given 'user', get free/busy details for the 'other' user." 482 483 filename = self.get_object_in_store(user, "freebusy-other", other) 484 485 if not filename or not isfile(filename): 486 periods = [] 487 else: 488 periods = map(lambda t: FreeBusyPeriod(*t), 489 self._get_table_atomic(user, filename)) 490 491 return FreeBusyCollection(periods, mutable) 492 493 def set_freebusy(self, user, freebusy, name=None): 494 495 "For the given 'user', set 'freebusy' details." 496 497 filename = self.get_object_in_store(user, name or "freebusy") 498 if not filename: 499 return False 500 501 self._set_table_atomic(user, filename, 502 map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy))) 503 return True 504 505 def set_freebusy_for_other(self, user, freebusy, other): 506 507 "For the given 'user', set 'freebusy' details for the 'other' user." 508 509 filename = self.get_object_in_store(user, "freebusy-other", other) 510 if not filename: 511 return False 512 513 self._set_table_atomic(user, filename, 514 map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy))) 515 return True 516 517 def get_freebusy_others(self, user): 518 519 """ 520 For the given 'user', return a list of other users for whom free/busy 521 information is retained. 522 """ 523 524 filename = self.get_object_in_store(user, "freebusy-other") 525 526 if not filename or not isdir(filename): 527 return [] 528 529 return listdir(filename) 530 531 # Tentative free/busy periods related to countering. 532 533 def get_freebusy_offers(self, user, mutable=False): 534 535 "Get free/busy offers for the given 'user'." 536 537 offers = [] 538 expired = [] 539 now = to_timezone(datetime.utcnow(), "UTC") 540 541 # Expire old offers and save the collection if modified. 542 543 self.acquire_lock(user) 544 try: 545 l = self.get_freebusy(user, "freebusy-offers") 546 for fb in l: 547 if fb.expires and get_datetime(fb.expires) <= now: 548 expired.append(fb) 549 else: 550 offers.append(fb) 551 552 if expired: 553 self.set_freebusy_offers(user, offers) 554 finally: 555 self.release_lock(user) 556 557 return FreeBusyCollection(offers, mutable) 558 559 # Requests and counter-proposals. 560 561 def _get_requests(self, user, queue): 562 563 "Get requests for the given 'user' from the given 'queue'." 564 565 filename = self.get_object_in_store(user, queue) 566 if not filename or not isfile(filename): 567 return [] 568 569 return self._get_table_atomic(user, filename, [(1, None), (2, None)]) 570 571 def get_requests(self, user): 572 573 "Get requests for the given 'user'." 574 575 return self._get_requests(user, "requests") 576 577 def _set_requests(self, user, requests, queue): 578 579 """ 580 For the given 'user', set the list of queued 'requests' in the given 581 'queue'. 582 """ 583 584 filename = self.get_object_in_store(user, queue) 585 if not filename: 586 return False 587 588 self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")]) 589 return True 590 591 def set_requests(self, user, requests): 592 593 "For the given 'user', set the list of queued 'requests'." 594 595 return self._set_requests(user, requests, "requests") 596 597 def _set_request(self, user, request, queue): 598 599 """ 600 For the given 'user', set the given 'request' in the given 'queue'. 601 """ 602 603 filename = self.get_object_in_store(user, queue) 604 if not filename: 605 return False 606 607 self.acquire_lock(user) 608 try: 609 f = codecs.open(filename, "ab", encoding="utf-8") 610 try: 611 self._set_table_item(f, request, [(1, ""), (2, "")]) 612 finally: 613 f.close() 614 fix_permissions(filename) 615 finally: 616 self.release_lock(user) 617 618 return True 619 620 def set_request(self, user, uid, recurrenceid=None, type=None): 621 622 """ 623 For the given 'user', set the queued 'uid' and 'recurrenceid', 624 indicating a request, along with any given 'type'. 625 """ 626 627 return self._set_request(user, (uid, recurrenceid, type), "requests") 628 629 def get_counters(self, user, uid, recurrenceid=None): 630 631 """ 632 For the given 'user', return a list of users from whom counter-proposals 633 have been received for the given 'uid' and optional 'recurrenceid'. 634 """ 635 636 filename = self.get_event_filename(user, uid, recurrenceid, "counters") 637 if not filename or not isdir(filename): 638 return [] 639 640 return [name for name in listdir(filename) if isfile(join(filename, name))] 641 642 def get_counter(self, user, other, uid, recurrenceid=None): 643 644 """ 645 For the given 'user', return the counter-proposal from 'other' for the 646 given 'uid' and optional 'recurrenceid'. 647 """ 648 649 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 650 if not filename or not isfile(filename): 651 return None 652 653 return self._get_object(user, filename) 654 655 def set_counter(self, user, other, node, uid, recurrenceid=None): 656 657 """ 658 For the given 'user', store a counter-proposal received from 'other' the 659 given 'node' representing that proposal for the given 'uid' and 660 'recurrenceid'. 661 """ 662 663 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 664 if not filename: 665 return False 666 667 return self._set_object(user, filename, node) 668 669 def remove_counters(self, user, uid, recurrenceid=None): 670 671 """ 672 For the given 'user', remove all counter-proposals associated with the 673 given 'uid' and 'recurrenceid'. 674 """ 675 676 filename = self.get_event_filename(user, uid, recurrenceid, "counters") 677 if not filename or not isdir(filename): 678 return False 679 680 removed = False 681 682 for other in listdir(filename): 683 counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 684 removed = removed or self._remove_object(counter_filename) 685 686 return removed 687 688 def remove_counter(self, user, other, uid, recurrenceid=None): 689 690 """ 691 For the given 'user', remove any counter-proposal from 'other' 692 associated with the given 'uid' and 'recurrenceid'. 693 """ 694 695 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 696 if not filename or not isfile(filename): 697 return False 698 699 return self._remove_object(filename) 700 701 # Event cancellation. 702 703 def cancel_event(self, user, uid, recurrenceid=None): 704 705 """ 706 Cancel an event for 'user' having the given 'uid'. If the optional 707 'recurrenceid' is specified, a specific instance or occurrence of an 708 event is cancelled. 709 """ 710 711 filename = self.get_event_filename(user, uid, recurrenceid) 712 cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 713 714 if filename and cancelled_filename and isfile(filename): 715 return self.move_object(filename, cancelled_filename) 716 717 return False 718 719 def uncancel_event(self, user, uid, recurrenceid=None): 720 721 """ 722 Uncancel an event for 'user' having the given 'uid'. If the optional 723 'recurrenceid' is specified, a specific instance or occurrence of an 724 event is uncancelled. 725 """ 726 727 filename = self.get_event_filename(user, uid, recurrenceid) 728 cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 729 730 if filename and cancelled_filename and isfile(cancelled_filename): 731 return self.move_object(cancelled_filename, filename) 732 733 return False 734 735 def remove_cancellation(self, user, uid, recurrenceid=None): 736 737 """ 738 Remove a cancellation for 'user' for the event having the given 'uid'. 739 If the optional 'recurrenceid' is specified, a specific instance or 740 occurrence of an event is affected. 741 """ 742 743 # Remove any parent event cancellation or a specific recurrence 744 # cancellation if indicated. 745 746 filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 747 748 if filename and isfile(filename): 749 return self._remove_object(filename) 750 751 return False 752 753 class Publisher(FileBase, PublisherBase): 754 755 "A publisher of objects." 756 757 def __init__(self, store_dir=None): 758 FileBase.__init__(self, store_dir or PUBLISH_DIR) 759 760 def set_freebusy(self, user, freebusy): 761 762 "For the given 'user', set 'freebusy' details." 763 764 filename = self.get_object_in_store(user, "freebusy") 765 if not filename: 766 return False 767 768 record = [] 769 rwrite = record.append 770 771 rwrite(("ORGANIZER", {}, user)) 772 rwrite(("UID", {}, user)) 773 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 774 775 for fb in freebusy: 776 if not fb.transp or fb.transp == "OPAQUE": 777 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 778 map(format_datetime, [fb.get_start_point(), fb.get_end_point()])))) 779 780 f = open(filename, "wb") 781 try: 782 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 783 finally: 784 f.close() 785 fix_permissions(filename) 786 787 return True 788 789 class Journal(FileStoreBase, JournalBase): 790 791 "A journal system to support quotas." 792 793 def __init__(self, store_dir=None): 794 FileBase.__init__(self, store_dir or JOURNAL_DIR) 795 796 # Quota and user identity/group discovery. 797 798 def get_quotas(self): 799 800 "Return a list of quotas." 801 802 return listdir(self.store_dir) 803 804 def get_quota_users(self, quota): 805 806 "Return a list of quota users." 807 808 filename = self.get_object_in_store(quota, "journal") 809 if not filename or not isdir(filename): 810 return [] 811 812 return listdir(filename) 813 814 # Groups of users sharing quotas. 815 816 def get_groups(self, quota): 817 818 "Return the identity mappings for the given 'quota' as a dictionary." 819 820 filename = self.get_object_in_store(quota, "groups") 821 if not filename or not isfile(filename): 822 return {} 823 824 return dict(self._get_table_atomic(quota, filename, tab_separated=False)) 825 826 def set_group(self, quota, store_user, user_group): 827 828 """ 829 For the given 'quota', set a mapping from 'store_user' to 'user_group'. 830 """ 831 832 filename = self.get_object_in_store(quota, "groups") 833 if not filename: 834 return False 835 836 groups = self.get_groups(quota) or {} 837 groups[store_user] = user_group 838 839 self._set_table_atomic(quota, filename, groups.items()) 840 return True 841 842 def get_limits(self, quota): 843 844 """ 845 Return the limits for the 'quota' as a dictionary mapping identities or 846 groups to durations. 847 """ 848 849 filename = self.get_object_in_store(quota, "limits") 850 if not filename or not isfile(filename): 851 return {} 852 853 return dict(self._get_table_atomic(quota, filename, tab_separated=False)) 854 855 def set_limit(self, quota, group, limit): 856 857 """ 858 For the given 'quota', set for a user 'group' the given 'limit' on 859 resource usage. 860 """ 861 862 filename = self.get_object_in_store(quota, "limits") 863 if not filename: 864 return False 865 866 limits = self.get_limits(quota) or {} 867 limits[group] = limit 868 869 self._set_table_atomic(quota, filename, limits.items()) 870 return True 871 872 # Free/busy period access for users within quota groups. 873 874 def get_freebusy_users(self, quota): 875 876 """ 877 Return a list of users whose free/busy details are retained for the 878 given 'quota'. 879 """ 880 881 filename = self.get_object_in_store(quota, "freebusy") 882 if not filename or not isdir(filename): 883 return [] 884 885 return listdir(filename) 886 887 def get_freebusy(self, quota, user, mutable=False): 888 889 "Get free/busy details for the given 'quota' and 'user'." 890 891 filename = self.get_object_in_store(quota, "freebusy", user) 892 893 if not filename or not isfile(filename): 894 periods = [] 895 else: 896 periods = map(lambda t: FreeBusyPeriod(*t), 897 self._get_table_atomic(quota, filename)) 898 899 return FreeBusyCollection(periods, mutable) 900 901 def set_freebusy(self, quota, user, freebusy): 902 903 "For the given 'quota' and 'user', set 'freebusy' details." 904 905 filename = self.get_object_in_store(quota, "freebusy", user) 906 if not filename: 907 return False 908 909 self._set_table_atomic(quota, filename, 910 map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy))) 911 return True 912 913 # Journal entry methods. 914 915 def get_entries(self, quota, group, mutable=False): 916 917 """ 918 Return a list of journal entries for the given 'quota' for the indicated 919 'group'. 920 """ 921 922 filename = self.get_object_in_store(quota, "journal", group) 923 924 if not filename or not isfile(filename): 925 periods = [] 926 else: 927 periods = map(lambda t: FreeBusyPeriod(*t), 928 self._get_table_atomic(quota, filename)) 929 930 return FreeBusyCollection(periods, mutable) 931 932 def set_entries(self, quota, group, entries): 933 934 """ 935 For the given 'quota' and indicated 'group', set the list of journal 936 'entries'. 937 """ 938 939 filename = self.get_object_in_store(quota, "journal", group) 940 if not filename: 941 return False 942 943 self._set_table_atomic(quota, filename, 944 map(lambda fb: fb.as_tuple(strings_only=True), list(entries))) 945 return True 946 947 # vim: tabstop=4 expandtab shiftwidth=4