imip-agent

imip_store.py

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