imip-agent

Annotated imip_store.py

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