imip-agent

imiptools/stores/file.py

1070:c1a7b48a82ff
2016-03-06 Paul Boddie Merged changes from the default branch. freebusy-collections
     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