imip-agent

Annotated imip_store.py

597:466eb0f72989
2015-07-25 Paul Boddie Changed the default store and publisher directory parameters so that None may be used to indicate usage of the default locations.
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@597 37
    def __init__(self, store_dir=None):
paul@597 38
        FileBase.__init__(self, store_dir or 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@563 341
        self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
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@563 352
        self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@112 353
        return True
paul@112 354
paul@142 355
    def _get_requests(self, user, queue):
paul@66 356
paul@142 357
        "Get requests for the given 'user' from the given 'queue'."
paul@66 358
paul@142 359
        filename = self.get_object_in_store(user, queue)
paul@81 360
        if not filename or not exists(filename):
paul@66 361
            return None
paul@66 362
paul@343 363
        return self._get_table(user, filename, [(1, None)])
paul@66 364
paul@142 365
    def get_requests(self, user):
paul@142 366
paul@142 367
        "Get requests for the given 'user'."
paul@142 368
paul@142 369
        return self._get_requests(user, "requests")
paul@142 370
paul@142 371
    def get_cancellations(self, user):
paul@142 372
paul@142 373
        "Get cancellations for the given 'user'."
paul@55 374
paul@142 375
        return self._get_requests(user, "cancellations")
paul@142 376
paul@142 377
    def _set_requests(self, user, requests, queue):
paul@66 378
paul@142 379
        """
paul@142 380
        For the given 'user', set the list of queued 'requests' in the given
paul@142 381
        'queue'.
paul@142 382
        """
paul@142 383
paul@142 384
        filename = self.get_object_in_store(user, queue)
paul@66 385
        if not filename:
paul@66 386
            return False
paul@66 387
paul@303 388
        self.acquire_lock(user)
paul@66 389
        try:
paul@303 390
            f = open(filename, "w")
paul@303 391
            try:
paul@303 392
                for request in requests:
paul@343 393
                    print >>f, "\t".join([value or "" for value in request])
paul@303 394
            finally:
paul@303 395
                f.close()
paul@303 396
                fix_permissions(filename)
paul@66 397
        finally:
paul@303 398
            self.release_lock(user)
paul@66 399
paul@66 400
        return True
paul@66 401
paul@142 402
    def set_requests(self, user, requests):
paul@142 403
paul@142 404
        "For the given 'user', set the list of queued 'requests'."
paul@142 405
paul@142 406
        return self._set_requests(user, requests, "requests")
paul@142 407
paul@142 408
    def set_cancellations(self, user, cancellations):
paul@66 409
paul@142 410
        "For the given 'user', set the list of queued 'cancellations'."
paul@142 411
paul@142 412
        return self._set_requests(user, cancellations, "cancellations")
paul@55 413
paul@343 414
    def _set_request(self, user, uid, recurrenceid, queue):
paul@142 415
paul@343 416
        """
paul@343 417
        For the given 'user', set the queued 'uid' and 'recurrenceid' in the
paul@343 418
        given 'queue'.
paul@343 419
        """
paul@142 420
paul@142 421
        filename = self.get_object_in_store(user, queue)
paul@55 422
        if not filename:
paul@55 423
            return False
paul@55 424
paul@303 425
        self.acquire_lock(user)
paul@55 426
        try:
paul@303 427
            f = open(filename, "a")
paul@303 428
            try:
paul@343 429
                print >>f, "\t".join([uid, recurrenceid or ""])
paul@303 430
            finally:
paul@303 431
                f.close()
paul@303 432
                fix_permissions(filename)
paul@55 433
        finally:
paul@303 434
            self.release_lock(user)
paul@55 435
paul@55 436
        return True
paul@55 437
paul@343 438
    def set_request(self, user, uid, recurrenceid=None):
paul@142 439
paul@343 440
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 441
paul@343 442
        return self._set_request(user, uid, recurrenceid, "requests")
paul@142 443
paul@343 444
    def set_cancellation(self, user, uid, recurrenceid=None):
paul@343 445
paul@343 446
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 447
paul@343 448
        return self._set_request(user, uid, recurrenceid, "cancellations")
paul@142 449
paul@343 450
    def queue_request(self, user, uid, recurrenceid=None):
paul@142 451
paul@343 452
        """
paul@343 453
        Queue a request for 'user' having the given 'uid'. If the optional
paul@343 454
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 455
        or occurrence of an event.
paul@343 456
        """
paul@66 457
paul@81 458
        requests = self.get_requests(user) or []
paul@66 459
paul@343 460
        if (uid, recurrenceid) not in requests:
paul@343 461
            return self.set_request(user, uid, recurrenceid)
paul@66 462
paul@66 463
        return False
paul@66 464
paul@343 465
    def dequeue_request(self, user, uid, recurrenceid=None):
paul@105 466
paul@343 467
        """
paul@343 468
        Dequeue a request for 'user' having the given 'uid'. If the optional
paul@343 469
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 470
        or occurrence of an event.
paul@343 471
        """
paul@105 472
paul@105 473
        requests = self.get_requests(user) or []
paul@105 474
paul@105 475
        try:
paul@343 476
            requests.remove((uid, recurrenceid))
paul@105 477
            self.set_requests(user, requests)
paul@105 478
        except ValueError:
paul@105 479
            return False
paul@105 480
        else:
paul@105 481
            return True
paul@105 482
paul@343 483
    def cancel_event(self, user, uid, recurrenceid=None):
paul@142 484
paul@343 485
        """
paul@343 486
        Queue an event for cancellation for 'user' having the given 'uid'. If
paul@343 487
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 488
        occurrence of an event is cancelled.
paul@343 489
        """
paul@142 490
paul@142 491
        cancellations = self.get_cancellations(user) or []
paul@142 492
paul@343 493
        if (uid, recurrenceid) not in cancellations:
paul@343 494
            return self.set_cancellation(user, uid, recurrenceid)
paul@142 495
paul@142 496
        return False
paul@142 497
paul@30 498
class FilePublisher(FileBase):
paul@30 499
paul@30 500
    "A publisher of objects."
paul@30 501
paul@597 502
    def __init__(self, store_dir=None):
paul@597 503
        FileBase.__init__(self, store_dir or PUBLISH_DIR)
paul@30 504
paul@30 505
    def set_freebusy(self, user, freebusy):
paul@30 506
paul@30 507
        "For the given 'user', set 'freebusy' details."
paul@30 508
paul@52 509
        filename = self.get_object_in_store(user, "freebusy")
paul@30 510
        if not filename:
paul@30 511
            return False
paul@30 512
paul@30 513
        record = []
paul@30 514
        rwrite = record.append
paul@30 515
paul@30 516
        rwrite(("ORGANIZER", {}, user))
paul@30 517
        rwrite(("UID", {}, user))
paul@30 518
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 519
paul@458 520
        for fb in freebusy:
paul@458 521
            if not fb.transp or fb.transp == "OPAQUE":
paul@529 522
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
paul@563 523
                    map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
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