imip-agent

Annotated imip_store.py

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