imip-agent

imip_store.py

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