imip-agent

Annotated imip_store.py

828:a7cd403b328b
2015-10-13 Paul Boddie Fixed encoding of None values produced by vCalendar parsing.
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@740 25
from imiptools.dates import format_datetime, get_datetime, to_timezone
paul@147 26
from imiptools.filesys import fix_permissions, FileBase
paul@458 27
from imiptools.period import FreeBusyPeriod
paul@808 28
from os.path import isdir, 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@648 46
    # Utility methods.
paul@648 47
paul@343 48
    def _set_defaults(self, t, empty_defaults):
paul@343 49
        for i, default in empty_defaults:
paul@343 50
            if i >= len(t):
paul@343 51
                t += [None] * (i - len(t) + 1)
paul@343 52
            if not t[i]:
paul@343 53
                t[i] = default
paul@343 54
        return t
paul@343 55
paul@343 56
    def _get_table(self, user, filename, empty_defaults=None):
paul@343 57
paul@343 58
        """
paul@343 59
        From the file for the given 'user' having the given 'filename', return
paul@343 60
        a list of tuples representing the file's contents.
paul@343 61
paul@343 62
        The 'empty_defaults' is a list of (index, value) tuples indicating the
paul@343 63
        default value where a column either does not exist or provides an empty
paul@343 64
        value.
paul@343 65
        """
paul@343 66
paul@702 67
        f = codecs.open(filename, "rb", encoding="utf-8")
paul@702 68
        try:
paul@702 69
            l = []
paul@702 70
            for line in f.readlines():
paul@702 71
                t = line.strip(" \r\n").split("\t")
paul@702 72
                if empty_defaults:
paul@702 73
                    t = self._set_defaults(t, empty_defaults)
paul@702 74
                l.append(tuple(t))
paul@702 75
            return l
paul@702 76
        finally:
paul@702 77
            f.close()
paul@702 78
paul@702 79
    def _get_table_atomic(self, user, filename, empty_defaults=None):
paul@702 80
paul@702 81
        """
paul@702 82
        From the file for the given 'user' having the given 'filename', return
paul@702 83
        a list of tuples representing the file's contents.
paul@702 84
paul@702 85
        The 'empty_defaults' is a list of (index, value) tuples indicating the
paul@702 86
        default value where a column either does not exist or provides an empty
paul@702 87
        value.
paul@702 88
        """
paul@702 89
paul@343 90
        self.acquire_lock(user)
paul@343 91
        try:
paul@702 92
            return self._get_table(user, filename, empty_defaults)
paul@343 93
        finally:
paul@343 94
            self.release_lock(user)
paul@343 95
paul@343 96
    def _set_table(self, user, filename, items, empty_defaults=None):
paul@343 97
paul@343 98
        """
paul@343 99
        For the given 'user', write to the file having the given 'filename' the
paul@343 100
        'items'.
paul@343 101
paul@343 102
        The 'empty_defaults' is a list of (index, value) tuples indicating the
paul@343 103
        default value where a column either does not exist or provides an empty
paul@343 104
        value.
paul@343 105
        """
paul@343 106
paul@702 107
        f = codecs.open(filename, "wb", encoding="utf-8")
paul@702 108
        try:
paul@702 109
            for item in items:
paul@747 110
                self._set_table_item(f, item, empty_defaults)
paul@702 111
        finally:
paul@702 112
            f.close()
paul@702 113
            fix_permissions(filename)
paul@702 114
paul@747 115
    def _set_table_item(self, f, item, empty_defaults=None):
paul@747 116
paul@747 117
        "Set in table 'f' the given 'item', using any 'empty_defaults'."
paul@747 118
paul@747 119
        if empty_defaults:
paul@747 120
            item = self._set_defaults(list(item), empty_defaults)
paul@747 121
        f.write("\t".join(item) + "\n")
paul@747 122
paul@702 123
    def _set_table_atomic(self, user, filename, items, empty_defaults=None):
paul@702 124
paul@702 125
        """
paul@702 126
        For the given 'user', write to the file having the given 'filename' the
paul@702 127
        'items'.
paul@702 128
paul@702 129
        The 'empty_defaults' is a list of (index, value) tuples indicating the
paul@702 130
        default value where a column either does not exist or provides an empty
paul@702 131
        value.
paul@702 132
        """
paul@702 133
paul@343 134
        self.acquire_lock(user)
paul@343 135
        try:
paul@702 136
            self._set_table(user, filename, items, empty_defaults)
paul@343 137
        finally:
paul@343 138
            self.release_lock(user)
paul@343 139
paul@648 140
    # Store object access.
paul@648 141
paul@329 142
    def _get_object(self, user, filename):
paul@329 143
paul@329 144
        """
paul@329 145
        Return the parsed object for the given 'user' having the given
paul@329 146
        'filename'.
paul@329 147
        """
paul@329 148
paul@329 149
        self.acquire_lock(user)
paul@329 150
        try:
paul@329 151
            f = open(filename, "rb")
paul@329 152
            try:
paul@329 153
                return parse_object(f, "utf-8")
paul@329 154
            finally:
paul@329 155
                f.close()
paul@329 156
        finally:
paul@329 157
            self.release_lock(user)
paul@329 158
paul@329 159
    def _set_object(self, user, filename, node):
paul@329 160
paul@329 161
        """
paul@329 162
        Set an object for the given 'user' having the given 'filename', using
paul@329 163
        'node' to define the object.
paul@329 164
        """
paul@329 165
paul@329 166
        self.acquire_lock(user)
paul@329 167
        try:
paul@329 168
            f = open(filename, "wb")
paul@329 169
            try:
paul@329 170
                to_stream(f, node)
paul@329 171
            finally:
paul@329 172
                f.close()
paul@329 173
                fix_permissions(filename)
paul@329 174
        finally:
paul@329 175
            self.release_lock(user)
paul@329 176
paul@329 177
        return True
paul@329 178
paul@329 179
    def _remove_object(self, filename):
paul@329 180
paul@329 181
        "Remove the object with the given 'filename'."
paul@329 182
paul@329 183
        try:
paul@329 184
            remove(filename)
paul@329 185
        except OSError:
paul@329 186
            return False
paul@329 187
paul@329 188
        return True
paul@329 189
paul@343 190
    def _remove_collection(self, filename):
paul@343 191
paul@343 192
        "Remove the collection with the given 'filename'."
paul@343 193
paul@343 194
        try:
paul@343 195
            rmdir(filename)
paul@343 196
        except OSError:
paul@343 197
            return False
paul@343 198
paul@343 199
        return True
paul@343 200
paul@670 201
    # User discovery.
paul@670 202
paul@670 203
    def get_users(self):
paul@670 204
paul@670 205
        "Return a list of users."
paul@670 206
paul@670 207
        return listdir(self.store_dir)
paul@670 208
paul@648 209
    # Event and event metadata access.
paul@648 210
paul@119 211
    def get_events(self, user):
paul@119 212
paul@119 213
        "Return a list of event identifiers."
paul@119 214
paul@138 215
        filename = self.get_object_in_store(user, "objects")
paul@808 216
        if not filename or not isdir(filename):
paul@119 217
            return None
paul@119 218
paul@119 219
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@119 220
paul@648 221
    def get_all_events(self, user):
paul@648 222
paul@648 223
        "Return a set of (uid, recurrenceid) tuples for all events."
paul@648 224
paul@648 225
        uids = self.get_events(user)
paul@674 226
        if not uids:
paul@674 227
            return set()
paul@648 228
paul@648 229
        all_events = set()
paul@648 230
        for uid in uids:
paul@648 231
            all_events.add((uid, None))
paul@648 232
            all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
paul@648 233
paul@648 234
        return all_events
paul@648 235
paul@760 236
    def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
paul@648 237
paul@694 238
        """
paul@694 239
        Get the filename providing the event for the given 'user' with the given
paul@694 240
        'uid'. If the optional 'recurrenceid' is specified, a specific instance
paul@694 241
        or occurrence of an event is returned.
paul@648 242
paul@694 243
        Where 'dirname' is specified, the given directory name is used as the
paul@694 244
        base of the location within which any filename will reside.
paul@694 245
        """
paul@648 246
paul@694 247
        if recurrenceid:
paul@760 248
            return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
paul@694 249
        else:
paul@760 250
            return self.get_complete_event_filename(user, uid, dirname, username)
paul@648 251
paul@343 252
    def get_event(self, user, uid, recurrenceid=None):
paul@343 253
paul@343 254
        """
paul@343 255
        Get the event for the given 'user' with the given 'uid'. If
paul@343 256
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 257
        occurrence of an event is returned.
paul@343 258
        """
paul@343 259
paul@694 260
        filename = self.get_event_filename(user, uid, recurrenceid)
paul@808 261
        if not filename or not isfile(filename):
paul@694 262
            return None
paul@694 263
paul@694 264
        return filename and self._get_object(user, filename)
paul@694 265
paul@760 266
    def get_complete_event_filename(self, user, uid, dirname=None, username=None):
paul@694 267
paul@694 268
        """
paul@694 269
        Get the filename providing the event for the given 'user' with the given
paul@694 270
        'uid'. 
paul@694 271
paul@694 272
        Where 'dirname' is specified, the given directory name is used as the
paul@694 273
        base of the location within which any filename will reside.
paul@760 274
paul@760 275
        Where 'username' is specified, the event details will reside in a file
paul@760 276
        bearing that name within a directory having 'uid' as its name.
paul@694 277
        """
paul@694 278
paul@760 279
        return self.get_object_in_store(user, dirname, "objects", uid, username)
paul@343 280
paul@343 281
    def get_complete_event(self, user, uid):
paul@50 282
paul@50 283
        "Get the event for the given 'user' with the given 'uid'."
paul@50 284
paul@694 285
        filename = self.get_complete_event_filename(user, uid)
paul@808 286
        if not filename or not isfile(filename):
paul@50 287
            return None
paul@50 288
paul@694 289
        return filename and self._get_object(user, filename)
paul@50 290
paul@343 291
    def set_event(self, user, uid, recurrenceid, node):
paul@343 292
paul@343 293
        """
paul@343 294
        Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
paul@343 295
        if the latter is specified, a specific instance or occurrence of an
paul@343 296
        event is referenced), using the given 'node' description.
paul@343 297
        """
paul@343 298
paul@343 299
        if recurrenceid:
paul@343 300
            return self.set_recurrence(user, uid, recurrenceid, node)
paul@343 301
        else:
paul@343 302
            return self.set_complete_event(user, uid, node)
paul@343 303
paul@343 304
    def set_complete_event(self, user, uid, node):
paul@50 305
paul@50 306
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@50 307
paul@138 308
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 309
        if not filename:
paul@50 310
            return False
paul@50 311
paul@329 312
        return self._set_object(user, filename, node)
paul@15 313
paul@365 314
    def remove_event(self, user, uid, recurrenceid=None):
paul@234 315
paul@343 316
        """
paul@343 317
        Remove an event for 'user' having the given 'uid'. If the optional
paul@343 318
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@343 319
        event is removed.
paul@343 320
        """
paul@343 321
paul@343 322
        if recurrenceid:
paul@343 323
            return self.remove_recurrence(user, uid, recurrenceid)
paul@343 324
        else:
paul@343 325
            for recurrenceid in self.get_recurrences(user, uid) or []:
paul@343 326
                self.remove_recurrence(user, uid, recurrenceid)
paul@343 327
            return self.remove_complete_event(user, uid)
paul@343 328
paul@343 329
    def remove_complete_event(self, user, uid):
paul@343 330
paul@234 331
        "Remove an event for 'user' having the given 'uid'."
paul@234 332
paul@378 333
        self.remove_recurrences(user, uid)
paul@369 334
paul@234 335
        filename = self.get_object_in_store(user, "objects", uid)
paul@234 336
        if not filename:
paul@234 337
            return False
paul@234 338
paul@329 339
        return self._remove_object(filename)
paul@234 340
paul@334 341
    def get_recurrences(self, user, uid):
paul@334 342
paul@334 343
        """
paul@334 344
        Get additional event instances for an event of the given 'user' with the
paul@694 345
        indicated 'uid'. Both active and cancelled recurrences are returned.
paul@694 346
        """
paul@694 347
paul@694 348
        return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
paul@694 349
paul@694 350
    def get_active_recurrences(self, user, uid):
paul@694 351
paul@694 352
        """
paul@694 353
        Get additional event instances for an event of the given 'user' with the
paul@694 354
        indicated 'uid'. Cancelled recurrences are not returned.
paul@334 355
        """
paul@334 356
paul@334 357
        filename = self.get_object_in_store(user, "recurrences", uid)
paul@808 358
        if not filename or not isdir(filename):
paul@347 359
            return []
paul@334 360
paul@334 361
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@334 362
paul@694 363
    def get_cancelled_recurrences(self, user, uid):
paul@694 364
paul@694 365
        """
paul@694 366
        Get additional event instances for an event of the given 'user' with the
paul@694 367
        indicated 'uid'. Only cancelled recurrences are returned.
paul@694 368
        """
paul@694 369
paul@782 370
        filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
paul@808 371
        if not filename or not isdir(filename):
paul@694 372
            return []
paul@694 373
paul@694 374
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@694 375
paul@760 376
    def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
paul@694 377
paul@694 378
        """
paul@694 379
        For the event of the given 'user' with the given 'uid', return the
paul@694 380
        filename providing the recurrence with the given 'recurrenceid'.
paul@694 381
paul@694 382
        Where 'dirname' is specified, the given directory name is used as the
paul@694 383
        base of the location within which any filename will reside.
paul@760 384
paul@760 385
        Where 'username' is specified, the event details will reside in a file
paul@760 386
        bearing that name within a directory having 'uid' as its name.
paul@694 387
        """
paul@694 388
paul@760 389
        return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
paul@694 390
paul@334 391
    def get_recurrence(self, user, uid, recurrenceid):
paul@334 392
paul@334 393
        """
paul@334 394
        For the event of the given 'user' with the given 'uid', return the
paul@334 395
        specific recurrence indicated by the 'recurrenceid'.
paul@334 396
        """
paul@334 397
paul@694 398
        filename = self.get_recurrence_filename(user, uid, recurrenceid)
paul@808 399
        if not filename or not isfile(filename):
paul@334 400
            return None
paul@334 401
paul@694 402
        return filename and self._get_object(user, filename)
paul@334 403
paul@334 404
    def set_recurrence(self, user, uid, recurrenceid, node):
paul@334 405
paul@334 406
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@334 407
paul@334 408
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 409
        if not filename:
paul@334 410
            return False
paul@334 411
paul@334 412
        return self._set_object(user, filename, node)
paul@334 413
paul@334 414
    def remove_recurrence(self, user, uid, recurrenceid):
paul@334 415
paul@378 416
        """
paul@378 417
        Remove a special recurrence from an event stored by 'user' having the
paul@378 418
        given 'uid' and 'recurrenceid'.
paul@378 419
        """
paul@334 420
paul@378 421
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 422
        if not filename:
paul@334 423
            return False
paul@334 424
paul@334 425
        return self._remove_object(filename)
paul@334 426
paul@378 427
    def remove_recurrences(self, user, uid):
paul@378 428
paul@378 429
        """
paul@378 430
        Remove all recurrences for an event stored by 'user' having the given
paul@378 431
        'uid'.
paul@378 432
        """
paul@378 433
paul@378 434
        for recurrenceid in self.get_recurrences(user, uid):
paul@378 435
            self.remove_recurrence(user, uid, recurrenceid)
paul@378 436
paul@378 437
        recurrences = self.get_object_in_store(user, "recurrences", uid)
paul@378 438
        if recurrences:
paul@378 439
            return self._remove_collection(recurrences)
paul@378 440
paul@378 441
        return True
paul@378 442
paul@652 443
    # Free/busy period providers, upon extension of the free/busy records.
paul@652 444
paul@672 445
    def _get_freebusy_providers(self, user):
paul@672 446
paul@672 447
        """
paul@672 448
        Return the free/busy providers for the given 'user'.
paul@672 449
paul@672 450
        This function returns any stored datetime and a list of providers as a
paul@672 451
        2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
paul@672 452
        """
paul@672 453
paul@672 454
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@808 455
        if not filename or not isfile(filename):
paul@672 456
            return None
paul@672 457
paul@672 458
        # Attempt to read providers, with a declaration of the datetime
paul@672 459
        # from which such providers are considered as still being active.
paul@672 460
paul@702 461
        t = self._get_table_atomic(user, filename, [(1, None)])
paul@672 462
        try:
paul@672 463
            dt_string = t[0][0]
paul@672 464
        except IndexError:
paul@672 465
            return None
paul@672 466
paul@672 467
        return dt_string, t[1:]
paul@672 468
paul@652 469
    def get_freebusy_providers(self, user, dt=None):
paul@652 470
paul@652 471
        """
paul@652 472
        Return a set of uncancelled events of the form (uid, recurrenceid)
paul@652 473
        providing free/busy details beyond the given datetime 'dt'.
paul@654 474
paul@654 475
        If 'dt' is not specified, all events previously found to provide
paul@654 476
        details will be returned. Otherwise, if 'dt' is earlier than the
paul@654 477
        datetime recorded for the known providers, None is returned, indicating
paul@654 478
        that the list of providers must be recomputed.
paul@672 479
paul@672 480
        This function returns a list of (uid, recurrenceid) tuples upon success.
paul@652 481
        """
paul@652 482
paul@672 483
        t = self._get_freebusy_providers(user)
paul@672 484
        if not t:
paul@672 485
            return None
paul@672 486
paul@672 487
        dt_string, t = t
paul@672 488
paul@672 489
        # If the requested datetime is earlier than the stated datetime, the
paul@672 490
        # providers will need to be recomputed.
paul@672 491
paul@672 492
        if dt:
paul@672 493
            providers_dt = get_datetime(dt_string)
paul@672 494
            if not providers_dt or providers_dt > dt:
paul@672 495
                return None
paul@672 496
paul@672 497
        # Otherwise, return the providers.
paul@672 498
paul@672 499
        return t[1:]
paul@672 500
paul@672 501
    def _set_freebusy_providers(self, user, dt_string, t):
paul@672 502
paul@672 503
        "Set the given provider timestamp 'dt_string' and table 't'."
paul@672 504
paul@652 505
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@672 506
        if not filename:
paul@672 507
            return False
paul@652 508
paul@672 509
        t.insert(0, (dt_string,))
paul@702 510
        self._set_table_atomic(user, filename, t, [(1, "")])
paul@672 511
        return True
paul@652 512
paul@654 513
    def set_freebusy_providers(self, user, dt, providers):
paul@654 514
paul@654 515
        """
paul@654 516
        Define the uncancelled events providing free/busy details beyond the
paul@654 517
        given datetime 'dt'.
paul@654 518
        """
paul@654 519
paul@672 520
        t = []
paul@654 521
paul@654 522
        for obj in providers:
paul@672 523
            t.append((obj.get_uid(), obj.get_recurrenceid()))
paul@672 524
paul@672 525
        return self._set_freebusy_providers(user, format_datetime(dt), t)
paul@654 526
paul@672 527
    def append_freebusy_provider(self, user, provider):
paul@672 528
paul@672 529
        "For the given 'user', append the free/busy 'provider'."
paul@672 530
paul@672 531
        t = self._get_freebusy_providers(user)
paul@672 532
        if not t:
paul@654 533
            return False
paul@654 534
paul@672 535
        dt_string, t = t
paul@672 536
        t.append((provider.get_uid(), provider.get_recurrenceid()))
paul@672 537
paul@672 538
        return self._set_freebusy_providers(user, dt_string, t)
paul@672 539
paul@672 540
    def remove_freebusy_provider(self, user, provider):
paul@672 541
paul@672 542
        "For the given 'user', remove the free/busy 'provider'."
paul@672 543
paul@672 544
        t = self._get_freebusy_providers(user)
paul@672 545
        if not t:
paul@672 546
            return False
paul@672 547
paul@672 548
        dt_string, t = t
paul@672 549
        try:
paul@672 550
            t.remove((provider.get_uid(), provider.get_recurrenceid()))
paul@672 551
        except ValueError:
paul@672 552
            return False
paul@672 553
paul@672 554
        return self._set_freebusy_providers(user, dt_string, t)
paul@654 555
paul@648 556
    # Free/busy period access.
paul@648 557
paul@702 558
    def get_freebusy(self, user, name=None, get_table=None):
paul@15 559
paul@15 560
        "Get free/busy details for the given 'user'."
paul@15 561
paul@702 562
        filename = self.get_object_in_store(user, name or "freebusy")
paul@808 563
        if not filename or not isfile(filename):
paul@167 564
            return []
paul@112 565
        else:
paul@702 566
            return map(lambda t: FreeBusyPeriod(*t),
paul@702 567
                (get_table or self._get_table_atomic)(user, filename, [(4, None)]))
paul@702 568
paul@702 569
    def get_freebusy_for_other(self, user, other, get_table=None):
paul@112 570
paul@112 571
        "For the given 'user', get free/busy details for the 'other' user."
paul@112 572
paul@112 573
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@808 574
        if not filename or not isfile(filename):
paul@167 575
            return []
paul@112 576
        else:
paul@702 577
            return map(lambda t: FreeBusyPeriod(*t),
paul@702 578
                (get_table or self._get_table_atomic)(user, filename, [(4, None)]))
paul@702 579
paul@702 580
    def set_freebusy(self, user, freebusy, name=None, set_table=None):
paul@15 581
paul@15 582
        "For the given 'user', set 'freebusy' details."
paul@15 583
paul@702 584
        filename = self.get_object_in_store(user, name or "freebusy")
paul@15 585
        if not filename:
paul@15 586
            return False
paul@15 587
paul@702 588
        (set_table or self._set_table_atomic)(user, filename,
paul@702 589
            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@15 590
        return True
paul@15 591
paul@702 592
    def set_freebusy_for_other(self, user, freebusy, other, set_table=None):
paul@110 593
paul@110 594
        "For the given 'user', set 'freebusy' details for the 'other' user."
paul@110 595
paul@110 596
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@110 597
        if not filename:
paul@110 598
            return False
paul@110 599
paul@702 600
        (set_table or self._set_table_atomic)(user, filename,
paul@702 601
            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@112 602
        return True
paul@112 603
paul@710 604
    # Tentative free/busy periods related to countering.
paul@710 605
paul@710 606
    def get_freebusy_offers(self, user):
paul@710 607
paul@710 608
        "Get free/busy offers for the given 'user'."
paul@710 609
paul@710 610
        offers = []
paul@710 611
        expired = []
paul@741 612
        now = to_timezone(datetime.utcnow(), "UTC")
paul@710 613
paul@710 614
        # Expire old offers and save the collection if modified.
paul@710 615
paul@730 616
        self.acquire_lock(user)
paul@710 617
        try:
paul@730 618
            l = self.get_freebusy(user, "freebusy-offers")
paul@710 619
            for fb in l:
paul@710 620
                if fb.expires and get_datetime(fb.expires) <= now:
paul@710 621
                    expired.append(fb)
paul@710 622
                else:
paul@710 623
                    offers.append(fb)
paul@710 624
paul@710 625
            if expired:
paul@730 626
                self.set_freebusy_offers(user, offers)
paul@710 627
        finally:
paul@730 628
            self.release_lock(user)
paul@710 629
paul@710 630
        return offers
paul@710 631
paul@710 632
    def set_freebusy_offers(self, user, freebusy):
paul@710 633
paul@710 634
        "For the given 'user', set 'freebusy' offers."
paul@710 635
paul@710 636
        return self.set_freebusy(user, freebusy, "freebusy-offers")
paul@710 637
paul@747 638
    # Requests and counter-proposals.
paul@648 639
paul@142 640
    def _get_requests(self, user, queue):
paul@66 641
paul@142 642
        "Get requests for the given 'user' from the given 'queue'."
paul@66 643
paul@142 644
        filename = self.get_object_in_store(user, queue)
paul@808 645
        if not filename or not isfile(filename):
paul@66 646
            return None
paul@66 647
paul@747 648
        return self._get_table_atomic(user, filename, [(1, None), (2, None)])
paul@66 649
paul@142 650
    def get_requests(self, user):
paul@142 651
paul@142 652
        "Get requests for the given 'user'."
paul@142 653
paul@142 654
        return self._get_requests(user, "requests")
paul@142 655
paul@142 656
    def _set_requests(self, user, requests, queue):
paul@66 657
paul@142 658
        """
paul@142 659
        For the given 'user', set the list of queued 'requests' in the given
paul@142 660
        'queue'.
paul@142 661
        """
paul@142 662
paul@142 663
        filename = self.get_object_in_store(user, queue)
paul@66 664
        if not filename:
paul@66 665
            return False
paul@66 666
paul@747 667
        self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])
paul@66 668
        return True
paul@66 669
paul@142 670
    def set_requests(self, user, requests):
paul@142 671
paul@142 672
        "For the given 'user', set the list of queued 'requests'."
paul@142 673
paul@142 674
        return self._set_requests(user, requests, "requests")
paul@142 675
paul@747 676
    def _set_request(self, user, request, queue):
paul@142 677
paul@343 678
        """
paul@747 679
        For the given 'user', set the given 'request' in the given 'queue'.
paul@343 680
        """
paul@142 681
paul@142 682
        filename = self.get_object_in_store(user, queue)
paul@55 683
        if not filename:
paul@55 684
            return False
paul@55 685
paul@303 686
        self.acquire_lock(user)
paul@55 687
        try:
paul@747 688
            f = codecs.open(filename, "ab", encoding="utf-8")
paul@303 689
            try:
paul@747 690
                self._set_table_item(f, request, [(1, ""), (2, "")])
paul@303 691
            finally:
paul@303 692
                f.close()
paul@303 693
                fix_permissions(filename)
paul@55 694
        finally:
paul@303 695
            self.release_lock(user)
paul@55 696
paul@55 697
        return True
paul@55 698
paul@747 699
    def set_request(self, user, uid, recurrenceid=None, type=None):
paul@142 700
paul@747 701
        """
paul@747 702
        For the given 'user', set the queued 'uid' and 'recurrenceid',
paul@747 703
        indicating a request, along with any given 'type'.
paul@747 704
        """
paul@142 705
paul@747 706
        return self._set_request(user, (uid, recurrenceid, type), "requests")
paul@747 707
paul@747 708
    def queue_request(self, user, uid, recurrenceid=None, type=None):
paul@142 709
paul@343 710
        """
paul@343 711
        Queue a request for 'user' having the given 'uid'. If the optional
paul@747 712
        'recurrenceid' is specified, the entry refers to a specific instance
paul@747 713
        or occurrence of an event. The 'type' parameter can be used to indicate
paul@747 714
        a specific type of request.
paul@747 715
        """
paul@747 716
paul@747 717
        requests = self.get_requests(user) or []
paul@747 718
paul@747 719
        if not self.have_request(requests, uid, recurrenceid):
paul@747 720
            return self.set_request(user, uid, recurrenceid, type)
paul@747 721
paul@747 722
        return False
paul@747 723
paul@747 724
    def dequeue_request(self, user, uid, recurrenceid=None, type=None):
paul@747 725
paul@747 726
        """
paul@747 727
        Dequeue all requests for 'user' having the given 'uid'. If the optional
paul@747 728
        'recurrenceid' is specified, all requests for that specific instance or
paul@747 729
        occurrence of an event are dequeued.
paul@343 730
        """
paul@66 731
paul@81 732
        requests = self.get_requests(user) or []
paul@750 733
        result = []
paul@747 734
paul@750 735
        for request in requests:
paul@808 736
            if request[:2] != (uid, recurrenceid):
paul@750 737
                result.append(request)
paul@747 738
paul@750 739
        self.set_requests(user, result)
paul@747 740
        return True
paul@747 741
paul@817 742
    def has_request(self, user, uid, recurrenceid=None, type=None, strict=False):
paul@817 743
        return self.have_request(self.get_requests(user) or [], uid, recurrenceid, type, strict)
paul@817 744
paul@754 745
    def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False):
paul@754 746
paul@754 747
        """
paul@754 748
        Return whether 'requests' contains a request with the given 'uid' and
paul@754 749
        any specified 'recurrenceid' and 'type'. If 'strict' is set to a true
paul@754 750
        value, the precise type of the request must match; otherwise, any type
paul@754 751
        of request for the identified object may be matched.
paul@754 752
        """
paul@754 753
paul@750 754
        for request in requests:
paul@762 755
            if request[:2] == (uid, recurrenceid) and (
paul@762 756
                not strict or
paul@762 757
                not request[2:] and not type or
paul@762 758
                request[2:] and request[2] == type):
paul@762 759
paul@750 760
                return True
paul@762 761
paul@750 762
        return False
paul@747 763
paul@760 764
    def get_counters(self, user, uid, recurrenceid=None):
paul@754 765
paul@754 766
        """
paul@766 767
        For the given 'user', return a list of users from whom counter-proposals
paul@766 768
        have been received for the given 'uid' and optional 'recurrenceid'.
paul@754 769
        """
paul@754 770
paul@754 771
        filename = self.get_event_filename(user, uid, recurrenceid, "counters")
paul@808 772
        if not filename or not isdir(filename):
paul@754 773
            return False
paul@754 774
paul@766 775
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@760 776
paul@760 777
    def get_counter(self, user, other, uid, recurrenceid=None):
paul@105 778
paul@343 779
        """
paul@760 780
        For the given 'user', return the counter-proposal from 'other' for the
paul@760 781
        given 'uid' and optional 'recurrenceid'.
paul@760 782
        """
paul@760 783
paul@760 784
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@760 785
        if not filename:
paul@760 786
            return False
paul@760 787
paul@760 788
        return self._get_object(user, filename)
paul@760 789
paul@760 790
    def set_counter(self, user, other, node, uid, recurrenceid=None):
paul@760 791
paul@760 792
        """
paul@760 793
        For the given 'user', store a counter-proposal received from 'other' the
paul@760 794
        given 'node' representing that proposal for the given 'uid' and
paul@760 795
        'recurrenceid'.
paul@760 796
        """
paul@760 797
paul@760 798
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@760 799
        if not filename:
paul@760 800
            return False
paul@760 801
paul@760 802
        return self._set_object(user, filename, node)
paul@760 803
paul@760 804
    def remove_counters(self, user, uid, recurrenceid=None):
paul@760 805
paul@760 806
        """
paul@760 807
        For the given 'user', remove all counter-proposals associated with the
paul@760 808
        given 'uid' and 'recurrenceid'.
paul@343 809
        """
paul@105 810
paul@747 811
        filename = self.get_event_filename(user, uid, recurrenceid, "counters")
paul@808 812
        if not filename or not isdir(filename):
paul@747 813
            return False
paul@747 814
paul@760 815
        removed = False
paul@747 816
paul@760 817
        for other in listdir(filename):
paul@760 818
            counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@760 819
            removed = removed or self._remove_object(counter_filename)
paul@760 820
paul@760 821
        return removed
paul@760 822
paul@760 823
    def remove_counter(self, user, other, uid, recurrenceid=None):
paul@105 824
paul@747 825
        """
paul@760 826
        For the given 'user', remove any counter-proposal from 'other'
paul@760 827
        associated with the given 'uid' and 'recurrenceid'.
paul@747 828
        """
paul@747 829
paul@760 830
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@808 831
        if not filename or not isfile(filename):
paul@105 832
            return False
paul@747 833
paul@747 834
        return self._remove_object(filename)
paul@747 835
paul@747 836
    # Event cancellation.
paul@105 837
paul@343 838
    def cancel_event(self, user, uid, recurrenceid=None):
paul@142 839
paul@343 840
        """
paul@694 841
        Cancel an event for 'user' having the given 'uid'. If the optional
paul@694 842
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@694 843
        event is cancelled.
paul@343 844
        """
paul@142 845
paul@694 846
        filename = self.get_event_filename(user, uid, recurrenceid)
paul@694 847
        cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@142 848
paul@808 849
        if filename and cancelled_filename and isfile(filename):
paul@694 850
            return self.move_object(filename, cancelled_filename)
paul@142 851
paul@142 852
        return False
paul@142 853
paul@796 854
    def remove_cancellations(self, user, uid, recurrenceid=None):
paul@796 855
paul@796 856
        """
paul@796 857
        Remove cancellations for 'user' for any event having the given 'uid'. If
paul@796 858
        the optional 'recurrenceid' is specified, a specific instance or
paul@796 859
        occurrence of an event is affected.
paul@796 860
        """
paul@796 861
paul@796 862
        # Remove all recurrence cancellations if a general event is indicated.
paul@796 863
paul@796 864
        if not recurrenceid:
paul@796 865
            for _recurrenceid in self.get_cancelled_recurrences(user, uid):
paul@796 866
                self.remove_cancellation(user, uid, _recurrenceid)
paul@796 867
paul@796 868
        return self.remove_cancellation(user, uid, recurrenceid)
paul@796 869
paul@796 870
    def remove_cancellation(self, user, uid, recurrenceid=None):
paul@796 871
paul@796 872
        """
paul@796 873
        Remove a cancellation for 'user' for the event having the given 'uid'.
paul@796 874
        If the optional 'recurrenceid' is specified, a specific instance or
paul@796 875
        occurrence of an event is affected.
paul@796 876
        """
paul@796 877
paul@796 878
        # Remove any parent event cancellation or a specific recurrence
paul@796 879
        # cancellation if indicated.
paul@796 880
paul@796 881
        filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@796 882
paul@808 883
        if filename and isfile(filename):
paul@796 884
            return self._remove_object(filename)
paul@796 885
paul@796 886
        return False
paul@796 887
paul@30 888
class FilePublisher(FileBase):
paul@30 889
paul@30 890
    "A publisher of objects."
paul@30 891
paul@597 892
    def __init__(self, store_dir=None):
paul@597 893
        FileBase.__init__(self, store_dir or PUBLISH_DIR)
paul@30 894
paul@30 895
    def set_freebusy(self, user, freebusy):
paul@30 896
paul@30 897
        "For the given 'user', set 'freebusy' details."
paul@30 898
paul@52 899
        filename = self.get_object_in_store(user, "freebusy")
paul@30 900
        if not filename:
paul@30 901
            return False
paul@30 902
paul@30 903
        record = []
paul@30 904
        rwrite = record.append
paul@30 905
paul@30 906
        rwrite(("ORGANIZER", {}, user))
paul@30 907
        rwrite(("UID", {}, user))
paul@30 908
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 909
paul@458 910
        for fb in freebusy:
paul@458 911
            if not fb.transp or fb.transp == "OPAQUE":
paul@529 912
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
paul@563 913
                    map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
paul@30 914
paul@395 915
        f = open(filename, "wb")
paul@30 916
        try:
paul@30 917
            to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
paul@30 918
        finally:
paul@30 919
            f.close()
paul@103 920
            fix_permissions(filename)
paul@30 921
paul@30 922
        return True
paul@30 923
paul@2 924
# vim: tabstop=4 expandtab shiftwidth=4