imip-agent

Annotated imip_store.py

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