imip-agent

imip_store.py

374:f34c3333cc4f
2015-03-03 Paul Boddie Made organisers also attendees by default, since organisers are not allowed to have participation status. See: https://bugzilla.mozilla.org/show_bug.cgi?id=459683 recurring-events
     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.filesys import fix_permissions, FileBase    26 from os.path import exists, isfile, join    27 from os import listdir, remove, rmdir    28 from time import sleep    29     30 class FileStore(FileBase):    31     32     "A file store of tabular free/busy data and objects."    33     34     def __init__(self, store_dir=STORE_DIR):    35         FileBase.__init__(self, store_dir)    36     37     def acquire_lock(self, user, timeout=None):    38         FileBase.acquire_lock(self, timeout, user)    39     40     def release_lock(self, user):    41         FileBase.release_lock(self, user)    42     43     def _set_defaults(self, t, empty_defaults):    44         for i, default in empty_defaults:    45             if i >= len(t):    46                 t += [None] * (i - len(t) + 1)    47             if not t[i]:    48                 t[i] = default    49         return t    50     51     def _get_table(self, user, filename, empty_defaults=None):    52     53         """    54         From the file for the given 'user' having the given 'filename', return    55         a list of tuples representing the file's contents.    56     57         The 'empty_defaults' is a list of (index, value) tuples indicating the    58         default value where a column either does not exist or provides an empty    59         value.    60         """    61     62         self.acquire_lock(user)    63         try:    64             f = open(filename, "rb")    65             try:    66                 l = []    67                 for line in f.readlines():    68                     t = line.strip().split("\t")    69                     if empty_defaults:    70                         t = self._set_defaults(t, empty_defaults)    71                     l.append(tuple(t))    72                 return l    73             finally:    74                 f.close()    75         finally:    76             self.release_lock(user)    77     78     def _set_table(self, user, filename, items, empty_defaults=None):    79     80         """    81         For the given 'user', write to the file having the given 'filename' the    82         'items'.    83     84         The 'empty_defaults' is a list of (index, value) tuples indicating the    85         default value where a column either does not exist or provides an empty    86         value.    87         """    88     89         self.acquire_lock(user)    90         try:    91             f = open(filename, "wb")    92             try:    93                 for item in items:    94                     if empty_defaults:    95                         item = self._set_defaults(list(item), empty_defaults)    96                     f.write("\t".join(item) + "\n")    97             finally:    98                 f.close()    99                 fix_permissions(filename)   100         finally:   101             self.release_lock(user)   102    103     def _get_object(self, user, filename):   104    105         """   106         Return the parsed object for the given 'user' having the given   107         'filename'.   108         """   109    110         self.acquire_lock(user)   111         try:   112             f = open(filename, "rb")   113             try:   114                 return parse_object(f, "utf-8")   115             finally:   116                 f.close()   117         finally:   118             self.release_lock(user)   119    120     def _set_object(self, user, filename, node):   121    122         """   123         Set an object for the given 'user' having the given 'filename', using   124         'node' to define the object.   125         """   126    127         self.acquire_lock(user)   128         try:   129             f = open(filename, "wb")   130             try:   131                 to_stream(f, node)   132             finally:   133                 f.close()   134                 fix_permissions(filename)   135         finally:   136             self.release_lock(user)   137    138         return True   139    140     def _remove_object(self, filename):   141    142         "Remove the object with the given 'filename'."   143    144         try:   145             remove(filename)   146         except OSError:   147             return False   148    149         return True   150    151     def _remove_collection(self, filename):   152    153         "Remove the collection with the given 'filename'."   154    155         try:   156             rmdir(filename)   157         except OSError:   158             return False   159    160         return True   161    162     def get_events(self, user):   163    164         "Return a list of event identifiers."   165    166         filename = self.get_object_in_store(user, "objects")   167         if not filename or not exists(filename):   168             return None   169    170         return [name for name in listdir(filename) if isfile(join(filename, name))]   171    172     def get_event(self, user, uid, recurrenceid=None):   173    174         """   175         Get the event for the given 'user' with the given 'uid'. If   176         the optional 'recurrenceid' is specified, a specific instance or   177         occurrence of an event is returned.   178         """   179    180         if recurrenceid:   181             return self.get_recurrence(user, uid, recurrenceid)   182         else:   183             return self.get_complete_event(user, uid)   184    185     def get_complete_event(self, user, uid):   186    187         "Get the event for the given 'user' with the given 'uid'."   188    189         filename = self.get_object_in_store(user, "objects", uid)   190         if not filename or not exists(filename):   191             return None   192    193         return self._get_object(user, filename)   194    195     def set_event(self, user, uid, recurrenceid, node):   196    197         """   198         Set an event for 'user' having the given 'uid' and 'recurrenceid' (which   199         if the latter is specified, a specific instance or occurrence of an   200         event is referenced), using the given 'node' description.   201         """   202    203         if recurrenceid:   204             return self.set_recurrence(user, uid, recurrenceid, node)   205         else:   206             return self.set_complete_event(user, uid, node)   207    208     def set_complete_event(self, user, uid, node):   209    210         "Set an event for 'user' having the given 'uid' and 'node'."   211    212         filename = self.get_object_in_store(user, "objects", uid)   213         if not filename:   214             return False   215    216         return self._set_object(user, filename, node)   217    218     def remove_event(self, user, uid, recurrenceid=None):   219    220         """   221         Remove an event for 'user' having the given 'uid'. If the optional   222         'recurrenceid' is specified, a specific instance or occurrence of an   223         event is removed.   224         """   225    226         if recurrenceid:   227             return self.remove_recurrence(user, uid, recurrenceid)   228         else:   229             for recurrenceid in self.get_recurrences(user, uid) or []:   230                 self.remove_recurrence(user, uid, recurrenceid)   231             return self.remove_complete_event(user, uid)   232    233     def remove_complete_event(self, user, uid):   234    235         "Remove an event for 'user' having the given 'uid'."   236    237         recurrences = self.get_object_in_store(user, "recurrences", uid)   238         if recurrences:   239             self._remove_collection(recurrences)   240    241         filename = self.get_object_in_store(user, "objects", uid)   242         if not filename:   243             return False   244    245         return self._remove_object(filename)   246    247     def get_recurrences(self, user, uid):   248    249         """   250         Get additional event instances for an event of the given 'user' with the   251         indicated 'uid'.   252         """   253    254         filename = self.get_object_in_store(user, "recurrences", uid)   255         if not filename or not exists(filename):   256             return []   257    258         return [name for name in listdir(filename) if isfile(join(filename, name))]   259    260     def get_recurrence(self, user, uid, recurrenceid):   261    262         """   263         For the event of the given 'user' with the given 'uid', return the   264         specific recurrence indicated by the 'recurrenceid'.   265         """   266    267         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   268         if not filename or not exists(filename):   269             return None   270    271         return self._get_object(user, filename)   272    273     def set_recurrence(self, user, uid, recurrenceid, node):   274    275         "Set an event for 'user' having the given 'uid' and 'node'."   276    277         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   278         if not filename:   279             return False   280    281         return self._set_object(user, filename, node)   282    283     def remove_recurrence(self, user, uid, recurrenceid):   284    285         "Remove an event for 'user' having the given 'uid'."   286    287         filename = self.get_object_in_store(user, "recurrences", uid)   288         if not filename:   289             return False   290    291         return self._remove_object(filename)   292    293     def get_freebusy(self, user):   294    295         "Get free/busy details for the given 'user'."   296    297         filename = self.get_object_in_store(user, "freebusy")   298         if not filename or not exists(filename):   299             return []   300         else:   301             return self._get_table(user, filename, [(4, None)])   302    303     def get_freebusy_for_other(self, user, other):   304    305         "For the given 'user', get free/busy details for the 'other' user."   306    307         filename = self.get_object_in_store(user, "freebusy-other", other)   308         if not filename or not exists(filename):   309             return []   310         else:   311             return self._get_table(user, filename, [(4, None)])   312    313     def set_freebusy(self, user, freebusy):   314    315         "For the given 'user', set 'freebusy' details."   316    317         filename = self.get_object_in_store(user, "freebusy")   318         if not filename:   319             return False   320    321         self._set_table(user, filename, freebusy, [(3, "OPAQUE"), (4, "")])   322         return True   323    324     def set_freebusy_for_other(self, user, freebusy, other):   325    326         "For the given 'user', set 'freebusy' details for the 'other' user."   327    328         filename = self.get_object_in_store(user, "freebusy-other", other)   329         if not filename:   330             return False   331    332         self._set_table(user, filename, freebusy, [(2, ""), (3, "OPAQUE"), (4, "")])   333         return True   334    335     def _get_requests(self, user, queue):   336    337         "Get requests for the given 'user' from the given 'queue'."   338    339         filename = self.get_object_in_store(user, queue)   340         if not filename or not exists(filename):   341             return None   342    343         return self._get_table(user, filename, [(1, None)])   344    345     def get_requests(self, user):   346    347         "Get requests for the given 'user'."   348    349         return self._get_requests(user, "requests")   350    351     def get_cancellations(self, user):   352    353         "Get cancellations for the given 'user'."   354    355         return self._get_requests(user, "cancellations")   356    357     def _set_requests(self, user, requests, queue):   358    359         """   360         For the given 'user', set the list of queued 'requests' in the given   361         'queue'.   362         """   363    364         filename = self.get_object_in_store(user, queue)   365         if not filename:   366             return False   367    368         self.acquire_lock(user)   369         try:   370             f = open(filename, "w")   371             try:   372                 for request in requests:   373                     print >>f, "\t".join([value or "" for value in request])   374             finally:   375                 f.close()   376                 fix_permissions(filename)   377         finally:   378             self.release_lock(user)   379    380         return True   381    382     def set_requests(self, user, requests):   383    384         "For the given 'user', set the list of queued 'requests'."   385    386         return self._set_requests(user, requests, "requests")   387    388     def set_cancellations(self, user, cancellations):   389    390         "For the given 'user', set the list of queued 'cancellations'."   391    392         return self._set_requests(user, cancellations, "cancellations")   393    394     def _set_request(self, user, uid, recurrenceid, queue):   395    396         """   397         For the given 'user', set the queued 'uid' and 'recurrenceid' in the   398         given 'queue'.   399         """   400    401         filename = self.get_object_in_store(user, queue)   402         if not filename:   403             return False   404    405         self.acquire_lock(user)   406         try:   407             f = open(filename, "a")   408             try:   409                 print >>f, "\t".join([uid, recurrenceid or ""])   410             finally:   411                 f.close()   412                 fix_permissions(filename)   413         finally:   414             self.release_lock(user)   415    416         return True   417    418     def set_request(self, user, uid, recurrenceid=None):   419    420         "For the given 'user', set the queued 'uid' and 'recurrenceid'."   421    422         return self._set_request(user, uid, recurrenceid, "requests")   423    424     def set_cancellation(self, user, uid, recurrenceid=None):   425    426         "For the given 'user', set the queued 'uid' and 'recurrenceid'."   427    428         return self._set_request(user, uid, recurrenceid, "cancellations")   429    430     def queue_request(self, user, uid, recurrenceid=None):   431    432         """   433         Queue a request for 'user' having the given 'uid'. If the optional   434         'recurrenceid' is specified, the request refers to a specific instance   435         or occurrence of an event.   436         """   437    438         requests = self.get_requests(user) or []   439    440         if (uid, recurrenceid) not in requests:   441             return self.set_request(user, uid, recurrenceid)   442    443         return False   444    445     def dequeue_request(self, user, uid, recurrenceid=None):   446    447         """   448         Dequeue a request for 'user' having the given 'uid'. If the optional   449         'recurrenceid' is specified, the request refers to a specific instance   450         or occurrence of an event.   451         """   452    453         requests = self.get_requests(user) or []   454    455         try:   456             requests.remove((uid, recurrenceid))   457             self.set_requests(user, requests)   458         except ValueError:   459             return False   460         else:   461             return True   462    463     def cancel_event(self, user, uid, recurrenceid=None):   464    465         """   466         Queue an event for cancellation for 'user' having the given 'uid'. If   467         the optional 'recurrenceid' is specified, a specific instance or   468         occurrence of an event is cancelled.   469         """   470    471         cancellations = self.get_cancellations(user) or []   472    473         if (uid, recurrenceid) not in cancellations:   474             return self.set_cancellation(user, uid, recurrenceid)   475    476         return False   477    478 class FilePublisher(FileBase):   479    480     "A publisher of objects."   481    482     def __init__(self, store_dir=PUBLISH_DIR):   483         FileBase.__init__(self, store_dir)   484    485     def set_freebusy(self, user, freebusy):   486    487         "For the given 'user', set 'freebusy' details."   488    489         filename = self.get_object_in_store(user, "freebusy")   490         if not filename:   491             return False   492    493         record = []   494         rwrite = record.append   495    496         rwrite(("ORGANIZER", {}, user))   497         rwrite(("UID", {}, user))   498         rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))   499    500         for start, end, uid, transp, recurrenceid in freebusy:   501             if not transp or transp == "OPAQUE":   502                 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end])))   503    504         f = open(filename, "w")   505         try:   506             to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))   507         finally:   508             f.close()   509             fix_permissions(filename)   510    511         return True   512    513 # vim: tabstop=4 expandtab shiftwidth=4