imip-agent

imip_store.py

381:f5fe45d84ecc
2015-03-05 Paul Boddie Changed complete event handling to always remove special/additional recurrences and to remove free/busy information for existing additional recurrences in anticipation of updated additional recurrence information. 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         self.remove_recurrences(user, uid)   238    239         filename = self.get_object_in_store(user, "objects", uid)   240         if not filename:   241             return False   242    243         return self._remove_object(filename)   244    245     def get_recurrences(self, user, uid):   246    247         """   248         Get additional event instances for an event of the given 'user' with the   249         indicated 'uid'.   250         """   251    252         filename = self.get_object_in_store(user, "recurrences", uid)   253         if not filename or not exists(filename):   254             return []   255    256         return [name for name in listdir(filename) if isfile(join(filename, name))]   257    258     def get_recurrence(self, user, uid, recurrenceid):   259    260         """   261         For the event of the given 'user' with the given 'uid', return the   262         specific recurrence indicated by the 'recurrenceid'.   263         """   264    265         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   266         if not filename or not exists(filename):   267             return None   268    269         return self._get_object(user, filename)   270    271     def set_recurrence(self, user, uid, recurrenceid, node):   272    273         "Set an event for 'user' having the given 'uid' and 'node'."   274    275         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   276         if not filename:   277             return False   278    279         return self._set_object(user, filename, node)   280    281     def remove_recurrence(self, user, uid, recurrenceid):   282    283         """   284         Remove a special recurrence from an event stored by 'user' having the   285         given 'uid' and 'recurrenceid'.   286         """   287    288         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   289         if not filename:   290             return False   291    292         return self._remove_object(filename)   293    294     def remove_recurrences(self, user, uid):   295    296         """   297         Remove all recurrences for an event stored by 'user' having the given   298         'uid'.   299         """   300    301         for recurrenceid in self.get_recurrences(user, uid):   302             self.remove_recurrence(user, uid, recurrenceid)   303    304         recurrences = self.get_object_in_store(user, "recurrences", uid)   305         if recurrences:   306             return self._remove_collection(recurrences)   307    308         return True   309    310     def get_freebusy(self, user):   311    312         "Get free/busy details for the given 'user'."   313    314         filename = self.get_object_in_store(user, "freebusy")   315         if not filename or not exists(filename):   316             return []   317         else:   318             return self._get_table(user, filename, [(4, None)])   319    320     def get_freebusy_for_other(self, user, other):   321    322         "For the given 'user', get free/busy details for the 'other' user."   323    324         filename = self.get_object_in_store(user, "freebusy-other", other)   325         if not filename or not exists(filename):   326             return []   327         else:   328             return self._get_table(user, filename, [(4, None)])   329    330     def set_freebusy(self, user, freebusy):   331    332         "For the given 'user', set 'freebusy' details."   333    334         filename = self.get_object_in_store(user, "freebusy")   335         if not filename:   336             return False   337    338         self._set_table(user, filename, freebusy, [(3, "OPAQUE"), (4, "")])   339         return True   340    341     def set_freebusy_for_other(self, user, freebusy, other):   342    343         "For the given 'user', set 'freebusy' details for the 'other' user."   344    345         filename = self.get_object_in_store(user, "freebusy-other", other)   346         if not filename:   347             return False   348    349         self._set_table(user, filename, freebusy, [(2, ""), (3, "OPAQUE"), (4, "")])   350         return True   351    352     def _get_requests(self, user, queue):   353    354         "Get requests for the given 'user' from the given 'queue'."   355    356         filename = self.get_object_in_store(user, queue)   357         if not filename or not exists(filename):   358             return None   359    360         return self._get_table(user, filename, [(1, None)])   361    362     def get_requests(self, user):   363    364         "Get requests for the given 'user'."   365    366         return self._get_requests(user, "requests")   367    368     def get_cancellations(self, user):   369    370         "Get cancellations for the given 'user'."   371    372         return self._get_requests(user, "cancellations")   373    374     def _set_requests(self, user, requests, queue):   375    376         """   377         For the given 'user', set the list of queued 'requests' in the given   378         'queue'.   379         """   380    381         filename = self.get_object_in_store(user, queue)   382         if not filename:   383             return False   384    385         self.acquire_lock(user)   386         try:   387             f = open(filename, "w")   388             try:   389                 for request in requests:   390                     print >>f, "\t".join([value or "" for value in request])   391             finally:   392                 f.close()   393                 fix_permissions(filename)   394         finally:   395             self.release_lock(user)   396    397         return True   398    399     def set_requests(self, user, requests):   400    401         "For the given 'user', set the list of queued 'requests'."   402    403         return self._set_requests(user, requests, "requests")   404    405     def set_cancellations(self, user, cancellations):   406    407         "For the given 'user', set the list of queued 'cancellations'."   408    409         return self._set_requests(user, cancellations, "cancellations")   410    411     def _set_request(self, user, uid, recurrenceid, queue):   412    413         """   414         For the given 'user', set the queued 'uid' and 'recurrenceid' in the   415         given 'queue'.   416         """   417    418         filename = self.get_object_in_store(user, queue)   419         if not filename:   420             return False   421    422         self.acquire_lock(user)   423         try:   424             f = open(filename, "a")   425             try:   426                 print >>f, "\t".join([uid, recurrenceid or ""])   427             finally:   428                 f.close()   429                 fix_permissions(filename)   430         finally:   431             self.release_lock(user)   432    433         return True   434    435     def set_request(self, user, uid, recurrenceid=None):   436    437         "For the given 'user', set the queued 'uid' and 'recurrenceid'."   438    439         return self._set_request(user, uid, recurrenceid, "requests")   440    441     def set_cancellation(self, user, uid, recurrenceid=None):   442    443         "For the given 'user', set the queued 'uid' and 'recurrenceid'."   444    445         return self._set_request(user, uid, recurrenceid, "cancellations")   446    447     def queue_request(self, user, uid, recurrenceid=None):   448    449         """   450         Queue a request for 'user' having the given 'uid'. If the optional   451         'recurrenceid' is specified, the request refers to a specific instance   452         or occurrence of an event.   453         """   454    455         requests = self.get_requests(user) or []   456    457         if (uid, recurrenceid) not in requests:   458             return self.set_request(user, uid, recurrenceid)   459    460         return False   461    462     def dequeue_request(self, user, uid, recurrenceid=None):   463    464         """   465         Dequeue a request for 'user' having the given 'uid'. If the optional   466         'recurrenceid' is specified, the request refers to a specific instance   467         or occurrence of an event.   468         """   469    470         requests = self.get_requests(user) or []   471    472         try:   473             requests.remove((uid, recurrenceid))   474             self.set_requests(user, requests)   475         except ValueError:   476             return False   477         else:   478             return True   479    480     def cancel_event(self, user, uid, recurrenceid=None):   481    482         """   483         Queue an event for cancellation for 'user' having the given 'uid'. If   484         the optional 'recurrenceid' is specified, a specific instance or   485         occurrence of an event is cancelled.   486         """   487    488         cancellations = self.get_cancellations(user) or []   489    490         if (uid, recurrenceid) not in cancellations:   491             return self.set_cancellation(user, uid, recurrenceid)   492    493         return False   494    495 class FilePublisher(FileBase):   496    497     "A publisher of objects."   498    499     def __init__(self, store_dir=PUBLISH_DIR):   500         FileBase.__init__(self, store_dir)   501    502     def set_freebusy(self, user, freebusy):   503    504         "For the given 'user', set 'freebusy' details."   505    506         filename = self.get_object_in_store(user, "freebusy")   507         if not filename:   508             return False   509    510         record = []   511         rwrite = record.append   512    513         rwrite(("ORGANIZER", {}, user))   514         rwrite(("UID", {}, user))   515         rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))   516    517         for start, end, uid, transp, recurrenceid in freebusy:   518             if not transp or transp == "OPAQUE":   519                 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end])))   520    521         f = open(filename, "w")   522         try:   523             to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))   524         finally:   525             f.close()   526             fix_permissions(filename)   527    528         return True   529    530 # vim: tabstop=4 expandtab shiftwidth=4