imip-agent

Annotated imip_store.py

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