imip-agent

Annotated imiptools/stores/file.py

1309:644b7e259059
2017-10-14 Paul Boddie Support BCC sending suppression so that routines requesting it can still be used with senders that will not support it, usually because there are no outgoing routing destinations for those senders.
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@1210 6
Copyright (C) 2014, 2015, 2016, 2017 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@1088 22
from imiptools.stores.common import StoreBase, PublisherBase, JournalBase
paul@1069 23
paul@30 24
from datetime import datetime
paul@1210 25
from imiptools.config import settings
paul@1232 26
from imiptools.data import Object, make_calendar, parse_object, to_stream
paul@740 27
from imiptools.dates import format_datetime, get_datetime, to_timezone
paul@147 28
from imiptools.filesys import fix_permissions, FileBase
paul@1236 29
paul@1236 30
from imiptools.freebusy import FreeBusyCollection, \
paul@1234 31
                               FreeBusyGroupCollection, \
paul@1236 32
                               FreeBusyOffersCollection, \
paul@1236 33
                               period_from_tuple, \
paul@1236 34
                               period_to_tuple
paul@1236 35
paul@1236 36
from imiptools.text import FileTable, FileTableDict, FileTableSingle, \
paul@1236 37
                           have_table
paul@1236 38
paul@808 39
from os.path import isdir, isfile, join
paul@343 40
from os import listdir, remove, rmdir
paul@15 41
paul@1230 42
# Obtain defaults from the settings.
paul@1230 43
paul@1210 44
STORE_DIR = settings["STORE_DIR"]
paul@1210 45
PUBLISH_DIR = settings["PUBLISH_DIR"]
paul@1210 46
JOURNAL_DIR = settings["JOURNAL_DIR"]
paul@1210 47
paul@1232 48
# Store classes.
paul@1232 49
paul@1039 50
class FileStoreBase(FileBase):
paul@50 51
paul@1236 52
    "A file store supporting user-specific locking."
paul@147 53
paul@303 54
    def acquire_lock(self, user, timeout=None):
paul@303 55
        FileBase.acquire_lock(self, timeout, user)
paul@303 56
paul@303 57
    def release_lock(self, user):
paul@303 58
        FileBase.release_lock(self, user)
paul@303 59
paul@1088 60
class Store(FileStoreBase, StoreBase):
paul@1039 61
paul@1039 62
    "A file store of tabular free/busy data and objects."
paul@1039 63
paul@1039 64
    def __init__(self, store_dir=None):
paul@1039 65
        FileBase.__init__(self, store_dir or STORE_DIR)
paul@1039 66
paul@648 67
    # Store object access.
paul@648 68
paul@329 69
    def _get_object(self, user, filename):
paul@329 70
paul@329 71
        """
paul@329 72
        Return the parsed object for the given 'user' having the given
paul@329 73
        'filename'.
paul@329 74
        """
paul@329 75
paul@329 76
        self.acquire_lock(user)
paul@329 77
        try:
paul@329 78
            f = open(filename, "rb")
paul@329 79
            try:
paul@1232 80
                return Object(parse_object(f, "utf-8"))
paul@329 81
            finally:
paul@329 82
                f.close()
paul@329 83
        finally:
paul@329 84
            self.release_lock(user)
paul@329 85
paul@329 86
    def _set_object(self, user, filename, node):
paul@329 87
paul@329 88
        """
paul@329 89
        Set an object for the given 'user' having the given 'filename', using
paul@329 90
        'node' to define the object.
paul@329 91
        """
paul@329 92
paul@329 93
        self.acquire_lock(user)
paul@329 94
        try:
paul@329 95
            f = open(filename, "wb")
paul@329 96
            try:
paul@329 97
                to_stream(f, node)
paul@329 98
            finally:
paul@329 99
                f.close()
paul@329 100
                fix_permissions(filename)
paul@329 101
        finally:
paul@329 102
            self.release_lock(user)
paul@329 103
paul@329 104
        return True
paul@329 105
paul@329 106
    def _remove_object(self, filename):
paul@329 107
paul@329 108
        "Remove the object with the given 'filename'."
paul@329 109
paul@329 110
        try:
paul@329 111
            remove(filename)
paul@329 112
        except OSError:
paul@329 113
            return False
paul@329 114
paul@329 115
        return True
paul@329 116
paul@343 117
    def _remove_collection(self, filename):
paul@343 118
paul@343 119
        "Remove the collection with the given 'filename'."
paul@343 120
paul@343 121
        try:
paul@343 122
            rmdir(filename)
paul@343 123
        except OSError:
paul@343 124
            return False
paul@343 125
paul@343 126
        return True
paul@343 127
paul@670 128
    # User discovery.
paul@670 129
paul@670 130
    def get_users(self):
paul@670 131
paul@670 132
        "Return a list of users."
paul@670 133
paul@670 134
        return listdir(self.store_dir)
paul@670 135
paul@648 136
    # Event and event metadata access.
paul@648 137
paul@119 138
    def get_events(self, user):
paul@119 139
paul@119 140
        "Return a list of event identifiers."
paul@119 141
paul@138 142
        filename = self.get_object_in_store(user, "objects")
paul@808 143
        if not filename or not isdir(filename):
paul@1142 144
            return []
paul@119 145
paul@119 146
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@119 147
paul@1142 148
    def get_cancelled_events(self, user):
paul@648 149
paul@1142 150
        "Return a list of event identifiers for cancelled events."
paul@648 151
paul@1142 152
        filename = self.get_object_in_store(user, "cancellations", "objects")
paul@1142 153
        if not filename or not isdir(filename):
paul@1142 154
            return []
paul@648 155
paul@1142 156
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@648 157
paul@858 158
    def get_event(self, user, uid, recurrenceid=None, dirname=None):
paul@343 159
paul@343 160
        """
paul@343 161
        Get the event for the given 'user' with the given 'uid'. If
paul@343 162
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 163
        occurrence of an event is returned.
paul@343 164
        """
paul@343 165
paul@858 166
        filename = self.get_event_filename(user, uid, recurrenceid, dirname)
paul@808 167
        if not filename or not isfile(filename):
paul@694 168
            return None
paul@694 169
paul@694 170
        return filename and self._get_object(user, filename)
paul@694 171
paul@343 172
    def get_complete_event(self, user, uid):
paul@50 173
paul@50 174
        "Get the event for the given 'user' with the given 'uid'."
paul@50 175
paul@694 176
        filename = self.get_complete_event_filename(user, uid)
paul@808 177
        if not filename or not isfile(filename):
paul@50 178
            return None
paul@50 179
paul@694 180
        return filename and self._get_object(user, filename)
paul@50 181
paul@343 182
    def set_complete_event(self, user, uid, node):
paul@50 183
paul@50 184
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@50 185
paul@138 186
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 187
        if not filename:
paul@50 188
            return False
paul@50 189
paul@329 190
        return self._set_object(user, filename, node)
paul@15 191
paul@1068 192
    def remove_parent_event(self, user, uid):
paul@1068 193
paul@1068 194
        "Remove the parent event for 'user' having the given 'uid'."
paul@369 195
paul@234 196
        filename = self.get_object_in_store(user, "objects", uid)
paul@234 197
        if not filename:
paul@234 198
            return False
paul@234 199
paul@329 200
        return self._remove_object(filename)
paul@234 201
paul@334 202
    def get_recurrences(self, user, uid):
paul@334 203
paul@334 204
        """
paul@334 205
        Get additional event instances for an event of the given 'user' with the
paul@694 206
        indicated 'uid'. Both active and cancelled recurrences are returned.
paul@694 207
        """
paul@694 208
paul@694 209
        return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
paul@694 210
paul@694 211
    def get_active_recurrences(self, user, uid):
paul@694 212
paul@694 213
        """
paul@694 214
        Get additional event instances for an event of the given 'user' with the
paul@694 215
        indicated 'uid'. Cancelled recurrences are not returned.
paul@334 216
        """
paul@334 217
paul@334 218
        filename = self.get_object_in_store(user, "recurrences", uid)
paul@808 219
        if not filename or not isdir(filename):
paul@347 220
            return []
paul@334 221
paul@334 222
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@334 223
paul@694 224
    def get_cancelled_recurrences(self, user, uid):
paul@694 225
paul@694 226
        """
paul@694 227
        Get additional event instances for an event of the given 'user' with the
paul@694 228
        indicated 'uid'. Only cancelled recurrences are returned.
paul@694 229
        """
paul@694 230
paul@782 231
        filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
paul@808 232
        if not filename or not isdir(filename):
paul@694 233
            return []
paul@694 234
paul@694 235
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@694 236
paul@334 237
    def get_recurrence(self, user, uid, recurrenceid):
paul@334 238
paul@334 239
        """
paul@334 240
        For the event of the given 'user' with the given 'uid', return the
paul@334 241
        specific recurrence indicated by the 'recurrenceid'.
paul@334 242
        """
paul@334 243
paul@694 244
        filename = self.get_recurrence_filename(user, uid, recurrenceid)
paul@808 245
        if not filename or not isfile(filename):
paul@334 246
            return None
paul@334 247
paul@694 248
        return filename and self._get_object(user, filename)
paul@334 249
paul@334 250
    def set_recurrence(self, user, uid, recurrenceid, node):
paul@334 251
paul@334 252
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@334 253
paul@334 254
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 255
        if not filename:
paul@334 256
            return False
paul@334 257
paul@334 258
        return self._set_object(user, filename, node)
paul@334 259
paul@334 260
    def remove_recurrence(self, user, uid, recurrenceid):
paul@334 261
paul@378 262
        """
paul@378 263
        Remove a special recurrence from an event stored by 'user' having the
paul@378 264
        given 'uid' and 'recurrenceid'.
paul@378 265
        """
paul@334 266
paul@378 267
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 268
        if not filename:
paul@334 269
            return False
paul@334 270
paul@334 271
        return self._remove_object(filename)
paul@334 272
paul@1068 273
    def remove_recurrence_collection(self, user, uid):
paul@1068 274
paul@1068 275
        """
paul@1068 276
        Remove the collection of recurrences stored by 'user' having the given
paul@1068 277
        'uid'.
paul@1068 278
        """
paul@1068 279
paul@378 280
        recurrences = self.get_object_in_store(user, "recurrences", uid)
paul@378 281
        if recurrences:
paul@378 282
            return self._remove_collection(recurrences)
paul@378 283
paul@378 284
        return True
paul@378 285
paul@1142 286
    # Event filename computation.
paul@1142 287
paul@1142 288
    def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
paul@1142 289
paul@1142 290
        """
paul@1142 291
        Get the filename providing the event for the given 'user' with the given
paul@1142 292
        'uid'. If the optional 'recurrenceid' is specified, a specific instance
paul@1142 293
        or occurrence of an event is returned.
paul@1142 294
paul@1142 295
        Where 'dirname' is specified, the given directory name is used as the
paul@1142 296
        base of the location within which any filename will reside.
paul@1142 297
        """
paul@1142 298
paul@1142 299
        if recurrenceid:
paul@1142 300
            return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
paul@1142 301
        else:
paul@1142 302
            return self.get_complete_event_filename(user, uid, dirname, username)
paul@1142 303
paul@1142 304
    def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
paul@1142 305
paul@1142 306
        """
paul@1142 307
        For the event of the given 'user' with the given 'uid', return the
paul@1142 308
        filename providing the recurrence with the given 'recurrenceid'.
paul@1142 309
paul@1142 310
        Where 'dirname' is specified, the given directory name is used as the
paul@1142 311
        base of the location within which any filename will reside.
paul@1142 312
paul@1142 313
        Where 'username' is specified, the event details will reside in a file
paul@1142 314
        bearing that name within a directory having 'uid' as its name.
paul@1142 315
        """
paul@1142 316
paul@1142 317
        return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
paul@1142 318
paul@1142 319
    def get_complete_event_filename(self, user, uid, dirname=None, username=None):
paul@1142 320
paul@1142 321
        """
paul@1142 322
        Get the filename providing the event for the given 'user' with the given
paul@1142 323
        'uid'. 
paul@1142 324
paul@1142 325
        Where 'dirname' is specified, the given directory name is used as the
paul@1142 326
        base of the location within which any filename will reside.
paul@1142 327
paul@1142 328
        Where 'username' is specified, the event details will reside in a file
paul@1142 329
        bearing that name within a directory having 'uid' as its name.
paul@1142 330
        """
paul@1142 331
paul@1142 332
        return self.get_object_in_store(user, dirname, "objects", uid, username)
paul@1142 333
paul@652 334
    # Free/busy period providers, upon extension of the free/busy records.
paul@652 335
paul@672 336
    def _get_freebusy_providers(self, user):
paul@672 337
paul@672 338
        """
paul@672 339
        Return the free/busy providers for the given 'user'.
paul@672 340
paul@672 341
        This function returns any stored datetime and a list of providers as a
paul@672 342
        2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
paul@672 343
        """
paul@672 344
paul@672 345
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@1236 346
        if not filename:
paul@672 347
            return None
paul@672 348
paul@672 349
        # Attempt to read providers, with a declaration of the datetime
paul@672 350
        # from which such providers are considered as still being active.
paul@672 351
paul@1236 352
        t = self._get_freebusy_providers_table(filename)
paul@1236 353
        header = t.get_header_values()
paul@1236 354
        if not header:
paul@672 355
            return None
paul@672 356
paul@1236 357
        return header[0], t
paul@1236 358
paul@1236 359
    def _get_freebusy_providers_table(self, filename):
paul@1236 360
paul@1236 361
        "Return a file-based table for storing providers in 'filename'."
paul@672 362
paul@1236 363
        return FileTable(filename,
paul@1236 364
                         in_defaults=[(1, None)],
paul@1236 365
                         out_defaults=[(1, "")],
paul@1236 366
                         headers=1)
paul@672 367
paul@1236 368
    def _set_freebusy_providers(self, user, dt_string, providers):
paul@1236 369
paul@1236 370
        "Set the given provider timestamp 'dt_string' and 'providers'."
paul@672 371
paul@652 372
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@672 373
        if not filename:
paul@672 374
            return False
paul@652 375
paul@1236 376
        self.acquire_lock(user)
paul@1236 377
        try:
paul@1236 378
            if not have_table(providers, filename):
paul@1236 379
                pr = self._get_freebusy_providers_table(filename)
paul@1236 380
                pr.replaceall(providers)
paul@1236 381
                providers = pr
paul@1236 382
            providers.set_header_values([dt_string])
paul@1236 383
            providers.close()
paul@1236 384
        finally:
paul@1236 385
            self.release_lock(user)
paul@672 386
        return True
paul@652 387
paul@648 388
    # Free/busy period access.
paul@648 389
paul@1236 390
    def get_freebusy(self, user, name=None, mutable=False):
paul@15 391
paul@15 392
        "Get free/busy details for the given 'user'."
paul@15 393
paul@702 394
        filename = self.get_object_in_store(user, name or "freebusy")
paul@1062 395
paul@1236 396
        if not filename:
paul@1236 397
            return []
paul@702 398
paul@1236 399
        return self._get_freebusy(filename, mutable, FreeBusyCollection)
paul@1071 400
paul@1236 401
    def get_freebusy_for_other(self, user, other, mutable=False, collection=None):
paul@112 402
paul@112 403
        "For the given 'user', get free/busy details for the 'other' user."
paul@112 404
paul@112 405
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@1062 406
paul@1236 407
        if not filename:
paul@1236 408
            return []
paul@1236 409
paul@1236 410
        return self._get_freebusy(filename, mutable, collection or FreeBusyCollection)
paul@1236 411
paul@1236 412
    def _get_freebusy(self, filename, mutable=False, collection=None):
paul@1236 413
paul@1236 414
        """
paul@1236 415
        Return a free/busy collection for 'filename' with the given 'mutable'
paul@1236 416
        condition, employing the specified 'collection' class.
paul@1236 417
        """
paul@702 418
paul@1192 419
        collection = collection or FreeBusyCollection
paul@1236 420
paul@1236 421
        periods = FileTable(filename, mutable=mutable,
paul@1236 422
                            in_converter=period_from_tuple(collection.period_class),
paul@1236 423
                            out_converter=period_to_tuple)
paul@1236 424
paul@1236 425
        return collection(periods, mutable=mutable)
paul@1071 426
paul@1064 427
    def set_freebusy(self, user, freebusy, name=None):
paul@15 428
paul@15 429
        "For the given 'user', set 'freebusy' details."
paul@15 430
paul@702 431
        filename = self.get_object_in_store(user, name or "freebusy")
paul@15 432
        if not filename:
paul@15 433
            return False
paul@15 434
paul@1236 435
        return self._set_freebusy(user, freebusy, filename)
paul@15 436
paul@1236 437
    def set_freebusy_for_other(self, user, freebusy, other, collection=None):
paul@110 438
paul@110 439
        "For the given 'user', set 'freebusy' details for the 'other' user."
paul@110 440
paul@110 441
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@110 442
        if not filename:
paul@110 443
            return False
paul@110 444
paul@1236 445
        return self._set_freebusy(user, freebusy, filename, collection)
paul@1236 446
paul@1236 447
    def _set_freebusy(self, user, freebusy, filename, collection=None):
paul@1236 448
paul@1236 449
        "For the given 'user', set 'freebusy' details for the given 'filename'."
paul@1236 450
paul@1236 451
        # Copy to the specified table if different from that given.
paul@1236 452
paul@1236 453
        self.acquire_lock(user)
paul@1236 454
        try:
paul@1236 455
            if not have_table(freebusy, filename):
paul@1236 456
                fbc = self._get_freebusy(filename, True, collection)
paul@1236 457
                fbc += freebusy
paul@1236 458
                freebusy = fbc
paul@1236 459
            freebusy.close()
paul@1236 460
        finally:
paul@1236 461
            self.release_lock(user)
paul@1236 462
paul@112 463
        return True
paul@112 464
paul@1142 465
    def get_freebusy_others(self, user):
paul@1142 466
paul@1142 467
        """
paul@1142 468
        For the given 'user', return a list of other users for whom free/busy
paul@1142 469
        information is retained.
paul@1142 470
        """
paul@1142 471
paul@1142 472
        filename = self.get_object_in_store(user, "freebusy-other")
paul@1142 473
paul@1142 474
        if not filename or not isdir(filename):
paul@1142 475
            return []
paul@1142 476
paul@1142 477
        return listdir(filename)
paul@1142 478
paul@710 479
    # Tentative free/busy periods related to countering.
paul@710 480
paul@1071 481
    def get_freebusy_offers(self, user, mutable=False):
paul@710 482
paul@710 483
        "Get free/busy offers for the given 'user'."
paul@710 484
paul@1236 485
        filename = self.get_object_in_store(user, "freebusy-offers")
paul@1236 486
paul@1236 487
        if not filename:
paul@1236 488
            return []
paul@1236 489
paul@710 490
        expired = []
paul@741 491
        now = to_timezone(datetime.utcnow(), "UTC")
paul@710 492
paul@710 493
        # Expire old offers and save the collection if modified.
paul@710 494
paul@730 495
        self.acquire_lock(user)
paul@710 496
        try:
paul@1236 497
            offers = self._get_freebusy(filename, True, FreeBusyOffersCollection)
paul@1236 498
            for fb in offers:
paul@710 499
                if fb.expires and get_datetime(fb.expires) <= now:
paul@1236 500
                    offers.remove(fb)
paul@710 501
            if expired:
paul@1236 502
                offers.close()
paul@710 503
        finally:
paul@730 504
            self.release_lock(user)
paul@710 505
paul@1236 506
        offers.mutable = mutable
paul@1236 507
        return offers
paul@710 508
paul@747 509
    # Requests and counter-proposals.
paul@648 510
paul@1236 511
    def get_requests(self, user, queue="requests"):
paul@66 512
paul@142 513
        "Get requests for the given 'user' from the given 'queue'."
paul@66 514
paul@142 515
        filename = self.get_object_in_store(user, queue)
paul@1236 516
        if not filename:
paul@1142 517
            return []
paul@66 518
paul@1236 519
        return FileTable(filename,
paul@1236 520
                         in_defaults=[(1, None), (2, None)],
paul@1236 521
                         out_defaults=[(1, ""), (2, "")])
paul@66 522
paul@1236 523
    def set_request(self, user, uid, recurrenceid=None, type=None):
paul@142 524
paul@1236 525
        """
paul@1236 526
        For the given 'user', set the queued 'uid' and 'recurrenceid',
paul@1236 527
        indicating a request, along with any given 'type'.
paul@1236 528
        """
paul@142 529
paul@1236 530
        requests = self.get_requests(user)
paul@1236 531
        return self.set_requests(user, [(uid, recurrenceid, type)])
paul@142 532
paul@1236 533
    def set_requests(self, user, requests, queue="requests"):
paul@66 534
paul@142 535
        """
paul@142 536
        For the given 'user', set the list of queued 'requests' in the given
paul@142 537
        'queue'.
paul@142 538
        """
paul@142 539
paul@142 540
        filename = self.get_object_in_store(user, queue)
paul@66 541
        if not filename:
paul@66 542
            return False
paul@66 543
paul@1236 544
        # Copy to the specified table if different from that given.
paul@55 545
paul@303 546
        self.acquire_lock(user)
paul@55 547
        try:
paul@1236 548
            if not have_table(requests, filename):
paul@1236 549
                req = self.get_requests(user, queue)
paul@1236 550
                req.replaceall(requests)
paul@1236 551
                requests = req
paul@1236 552
            requests.close()
paul@55 553
        finally:
paul@303 554
            self.release_lock(user)
paul@55 555
paul@55 556
        return True
paul@55 557
paul@760 558
    def get_counters(self, user, uid, recurrenceid=None):
paul@754 559
paul@754 560
        """
paul@766 561
        For the given 'user', return a list of users from whom counter-proposals
paul@766 562
        have been received for the given 'uid' and optional 'recurrenceid'.
paul@754 563
        """
paul@754 564
paul@754 565
        filename = self.get_event_filename(user, uid, recurrenceid, "counters")
paul@808 566
        if not filename or not isdir(filename):
paul@1142 567
            return []
paul@754 568
paul@766 569
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@760 570
paul@1305 571
    def get_counter_recurrences(self, user, uid):
paul@1305 572
paul@1305 573
        """
paul@1305 574
        For the given 'user', return a list of recurrence identifiers describing
paul@1305 575
        counter-proposals for the parent event with the given 'uid'.
paul@1305 576
        """
paul@1305 577
paul@1305 578
        filename = self.get_object_in_store(user, "counters", "recurrences", uid)
paul@1305 579
        if not filename or not isdir(filename):
paul@1305 580
            return []
paul@1305 581
paul@1305 582
        return listdir(filename)
paul@1305 583
paul@760 584
    def get_counter(self, user, other, uid, recurrenceid=None):
paul@105 585
paul@343 586
        """
paul@760 587
        For the given 'user', return the counter-proposal from 'other' for the
paul@760 588
        given 'uid' and optional 'recurrenceid'.
paul@760 589
        """
paul@760 590
paul@760 591
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@1090 592
        if not filename or not isfile(filename):
paul@1142 593
            return None
paul@760 594
paul@760 595
        return self._get_object(user, filename)
paul@760 596
paul@760 597
    def set_counter(self, user, other, node, uid, recurrenceid=None):
paul@760 598
paul@760 599
        """
paul@760 600
        For the given 'user', store a counter-proposal received from 'other' the
paul@760 601
        given 'node' representing that proposal for the given 'uid' and
paul@760 602
        'recurrenceid'.
paul@760 603
        """
paul@760 604
paul@760 605
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@760 606
        if not filename:
paul@760 607
            return False
paul@760 608
paul@760 609
        return self._set_object(user, filename, node)
paul@760 610
paul@1306 611
    def remove_counters(self, user, uid, recurrenceid=None, attendee=None):
paul@760 612
paul@760 613
        """
paul@760 614
        For the given 'user', remove all counter-proposals associated with the
paul@1306 615
        given 'uid' and 'recurrenceid'. If a parent event is specified, all
paul@1306 616
        recurrence counter-proposals will be removed. If 'attendee' is
paul@1306 617
        specified, only objects provided by this attendee will be removed.
paul@1306 618
        """
paul@1306 619
paul@1306 620
        self._remove_counters(user, uid, recurrenceid, attendee)
paul@1306 621
paul@1306 622
        if not recurrenceid:
paul@1306 623
            for recurrenceid in self.get_counter_recurrences(user, uid):
paul@1306 624
                self._remove_counters(user, uid, recurrenceid, attendee)
paul@1306 625
paul@1306 626
    def _remove_counters(self, user, uid, recurrenceid=None, attendee=None):
paul@1306 627
paul@1306 628
        """
paul@1306 629
        For the given 'user', remove all counter-proposals associated with the
paul@1306 630
        given 'uid' and 'recurrenceid'. If 'attendee' is specified, only objects
paul@1306 631
        provided by this attendee will be removed.
paul@343 632
        """
paul@105 633
paul@747 634
        filename = self.get_event_filename(user, uid, recurrenceid, "counters")
paul@808 635
        if not filename or not isdir(filename):
paul@747 636
            return False
paul@747 637
paul@760 638
        removed = False
paul@747 639
paul@760 640
        for other in listdir(filename):
paul@1306 641
            if not attendee or other == attendee:
paul@1306 642
                counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@1306 643
                removed = removed or self._remove_object(counter_filename)
paul@1306 644
paul@1306 645
        if not listdir(filename):
paul@1306 646
            self._remove_collection(filename)
paul@760 647
paul@760 648
        return removed
paul@760 649
paul@760 650
    def remove_counter(self, user, other, uid, recurrenceid=None):
paul@105 651
paul@747 652
        """
paul@760 653
        For the given 'user', remove any counter-proposal from 'other'
paul@760 654
        associated with the given 'uid' and 'recurrenceid'.
paul@747 655
        """
paul@747 656
paul@760 657
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@808 658
        if not filename or not isfile(filename):
paul@105 659
            return False
paul@747 660
paul@747 661
        return self._remove_object(filename)
paul@747 662
paul@747 663
    # Event cancellation.
paul@105 664
paul@343 665
    def cancel_event(self, user, uid, recurrenceid=None):
paul@142 666
paul@343 667
        """
paul@694 668
        Cancel an event for 'user' having the given 'uid'. If the optional
paul@694 669
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@694 670
        event is cancelled.
paul@343 671
        """
paul@142 672
paul@694 673
        filename = self.get_event_filename(user, uid, recurrenceid)
paul@694 674
        cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@142 675
paul@808 676
        if filename and cancelled_filename and isfile(filename):
paul@694 677
            return self.move_object(filename, cancelled_filename)
paul@142 678
paul@142 679
        return False
paul@142 680
paul@863 681
    def uncancel_event(self, user, uid, recurrenceid=None):
paul@863 682
paul@863 683
        """
paul@863 684
        Uncancel an event for 'user' having the given 'uid'. If the optional
paul@863 685
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@863 686
        event is uncancelled.
paul@863 687
        """
paul@863 688
paul@863 689
        filename = self.get_event_filename(user, uid, recurrenceid)
paul@863 690
        cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@863 691
paul@863 692
        if filename and cancelled_filename and isfile(cancelled_filename):
paul@863 693
            return self.move_object(cancelled_filename, filename)
paul@863 694
paul@863 695
        return False
paul@863 696
paul@796 697
    def remove_cancellation(self, user, uid, recurrenceid=None):
paul@796 698
paul@796 699
        """
paul@796 700
        Remove a cancellation for 'user' for the event having the given 'uid'.
paul@796 701
        If the optional 'recurrenceid' is specified, a specific instance or
paul@796 702
        occurrence of an event is affected.
paul@796 703
        """
paul@796 704
paul@796 705
        # Remove any parent event cancellation or a specific recurrence
paul@796 706
        # cancellation if indicated.
paul@796 707
paul@796 708
        filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@796 709
paul@808 710
        if filename and isfile(filename):
paul@796 711
            return self._remove_object(filename)
paul@796 712
paul@796 713
        return False
paul@796 714
paul@1088 715
class Publisher(FileBase, PublisherBase):
paul@30 716
paul@30 717
    "A publisher of objects."
paul@30 718
paul@597 719
    def __init__(self, store_dir=None):
paul@597 720
        FileBase.__init__(self, store_dir or PUBLISH_DIR)
paul@30 721
paul@30 722
    def set_freebusy(self, user, freebusy):
paul@30 723
paul@30 724
        "For the given 'user', set 'freebusy' details."
paul@30 725
paul@52 726
        filename = self.get_object_in_store(user, "freebusy")
paul@30 727
        if not filename:
paul@30 728
            return False
paul@30 729
paul@30 730
        record = []
paul@30 731
        rwrite = record.append
paul@30 732
paul@30 733
        rwrite(("ORGANIZER", {}, user))
paul@30 734
        rwrite(("UID", {}, user))
paul@30 735
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 736
paul@458 737
        for fb in freebusy:
paul@458 738
            if not fb.transp or fb.transp == "OPAQUE":
paul@529 739
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
paul@563 740
                    map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
paul@30 741
paul@395 742
        f = open(filename, "wb")
paul@30 743
        try:
paul@30 744
            to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
paul@30 745
        finally:
paul@30 746
            f.close()
paul@103 747
            fix_permissions(filename)
paul@30 748
paul@30 749
        return True
paul@30 750
paul@1192 751
class Journal(Store, JournalBase):
paul@1039 752
paul@1039 753
    "A journal system to support quotas."
paul@1039 754
paul@1049 755
    # Quota and user identity/group discovery.
paul@1049 756
paul@1194 757
    get_quotas = Store.get_users
paul@1194 758
    get_quota_users = Store.get_freebusy_others
paul@1176 759
paul@1176 760
    # Delegate information for the quota.
paul@1176 761
paul@1176 762
    def get_delegates(self, quota):
paul@1176 763
paul@1176 764
        "Return a list of delegates for 'quota'."
paul@1176 765
paul@1176 766
        filename = self.get_object_in_store(quota, "delegates")
paul@1236 767
        if not filename:
paul@1176 768
            return []
paul@1176 769
paul@1236 770
        return FileTableSingle(filename)
paul@1176 771
paul@1176 772
    def set_delegates(self, quota, delegates):
paul@1176 773
paul@1176 774
        "For the given 'quota', set the list of 'delegates'."
paul@1176 775
paul@1176 776
        filename = self.get_object_in_store(quota, "delegates")
paul@1176 777
        if not filename:
paul@1176 778
            return False
paul@1176 779
paul@1236 780
        self.acquire_lock(quota)
paul@1236 781
        try:
paul@1236 782
            if not have_table(delegates, filename):
paul@1236 783
                de = self.get_delegates(quota)
paul@1236 784
                de.replaceall(delegates)
paul@1236 785
                delegates = de
paul@1236 786
            delegates.close()
paul@1236 787
        finally:
paul@1236 788
            self.release_lock(quota)
paul@1236 789
paul@1176 790
        return True
paul@1049 791
paul@1039 792
    # Groups of users sharing quotas.
paul@1039 793
paul@1039 794
    def get_groups(self, quota):
paul@1039 795
paul@1039 796
        "Return the identity mappings for the given 'quota' as a dictionary."
paul@1039 797
paul@1039 798
        filename = self.get_object_in_store(quota, "groups")
paul@1236 799
        if not filename:
paul@1039 800
            return {}
paul@1039 801
paul@1236 802
        return FileTableDict(filename, tab_separated=False)
paul@1039 803
paul@1176 804
    def set_groups(self, quota, groups):
paul@1142 805
paul@1176 806
        "For the given 'quota', set 'groups' mapping users to groups."
paul@1142 807
paul@1142 808
        filename = self.get_object_in_store(quota, "groups")
paul@1142 809
        if not filename:
paul@1142 810
            return False
paul@1142 811
paul@1236 812
        self.acquire_lock(quota)
paul@1236 813
        try:
paul@1236 814
            if not have_table(groups, filename):
paul@1236 815
                gr = self.get_groups(quota)
paul@1236 816
                gr.updateall(groups)
paul@1236 817
                groups = gr
paul@1236 818
            groups.close()
paul@1236 819
        finally:
paul@1236 820
            self.release_lock(quota)
paul@1236 821
paul@1142 822
        return True
paul@1142 823
paul@1039 824
    def get_limits(self, quota):
paul@1039 825
paul@1039 826
        """
paul@1039 827
        Return the limits for the 'quota' as a dictionary mapping identities or
paul@1039 828
        groups to durations.
paul@1039 829
        """
paul@1039 830
paul@1039 831
        filename = self.get_object_in_store(quota, "limits")
paul@1236 832
        if not filename:
paul@1142 833
            return {}
paul@1039 834
paul@1236 835
        return FileTableDict(filename, tab_separated=False)
paul@1039 836
paul@1176 837
    def set_limits(self, quota, limits):
paul@1089 838
paul@1089 839
        """
paul@1176 840
        For the given 'quota', set the given 'limits' on resource usage mapping
paul@1176 841
        groups to limits.
paul@1089 842
        """
paul@1089 843
paul@1089 844
        filename = self.get_object_in_store(quota, "limits")
paul@1089 845
        if not filename:
paul@1142 846
            return False
paul@1089 847
paul@1236 848
        self.acquire_lock(quota)
paul@1236 849
        try:
paul@1236 850
            if not have_table(limits, filename):
paul@1236 851
                li = self.get_limits(quota)
paul@1236 852
                li.updateall(limits)
paul@1236 853
                limits = li
paul@1236 854
            limits.close()
paul@1236 855
        finally:
paul@1236 856
            self.release_lock(quota)
paul@1236 857
paul@1089 858
        return True
paul@1089 859
paul@1039 860
    # Journal entry methods.
paul@1039 861
paul@1071 862
    def get_entries(self, quota, group, mutable=False):
paul@1039 863
paul@1039 864
        """
paul@1039 865
        Return a list of journal entries for the given 'quota' for the indicated
paul@1039 866
        'group'.
paul@1039 867
        """
paul@1039 868
paul@1193 869
        return self.get_freebusy_for_other(quota, group, mutable)
paul@1039 870
paul@1039 871
    def set_entries(self, quota, group, entries):
paul@1039 872
paul@1039 873
        """
paul@1039 874
        For the given 'quota' and indicated 'group', set the list of journal
paul@1039 875
        'entries'.
paul@1039 876
        """
paul@1039 877
paul@1192 878
        return self.set_freebusy_for_other(quota, entries, group)
paul@1039 879
paul@1193 880
    # Compatibility methods.
paul@1193 881
paul@1193 882
    def get_freebusy_for_other(self, user, other, mutable=False):
paul@1236 883
        return Store.get_freebusy_for_other(self, user, other, mutable, collection=FreeBusyGroupCollection)
paul@1236 884
paul@1236 885
    def set_freebusy_for_other(self, user, entries, other):
paul@1236 886
        Store.set_freebusy_for_other(self, user, entries, other, collection=FreeBusyGroupCollection)
paul@1193 887
paul@2 888
# vim: tabstop=4 expandtab shiftwidth=4