imip-agent

Annotated imip_store.py

702:ceebd7a6e522
2015-09-07 Paul Boddie Added a compound locking mechanism to avoid update inconsistencies upon concurrent modification of data.
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@652 25
from imiptools.dates import format_datetime, get_datetime
paul@147 26
from imiptools.filesys import fix_permissions, FileBase
paul@458 27
from imiptools.period import FreeBusyPeriod
paul@147 28
from os.path import exists, isfile, join
paul@343 29
from os import listdir, remove, rmdir
paul@303 30
from time import sleep
paul@395 31
import codecs
paul@15 32
paul@50 33
class FileStore(FileBase):
paul@50 34
paul@50 35
    "A file store of tabular free/busy data and objects."
paul@50 36
paul@597 37
    def __init__(self, store_dir=None):
paul@597 38
        FileBase.__init__(self, store_dir or STORE_DIR)
paul@147 39
paul@303 40
    def acquire_lock(self, user, timeout=None):
paul@303 41
        FileBase.acquire_lock(self, timeout, user)
paul@303 42
paul@303 43
    def release_lock(self, user):
paul@303 44
        FileBase.release_lock(self, user)
paul@303 45
paul@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@702 110
                if empty_defaults:
paul@702 111
                    item = self._set_defaults(list(item), empty_defaults)
paul@702 112
                f.write("\t".join(item) + "\n")
paul@702 113
        finally:
paul@702 114
            f.close()
paul@702 115
            fix_permissions(filename)
paul@702 116
paul@702 117
    def _set_table_atomic(self, user, filename, items, empty_defaults=None):
paul@702 118
paul@702 119
        """
paul@702 120
        For the given 'user', write to the file having the given 'filename' the
paul@702 121
        'items'.
paul@702 122
paul@702 123
        The 'empty_defaults' is a list of (index, value) tuples indicating the
paul@702 124
        default value where a column either does not exist or provides an empty
paul@702 125
        value.
paul@702 126
        """
paul@702 127
paul@343 128
        self.acquire_lock(user)
paul@343 129
        try:
paul@702 130
            self._set_table(user, filename, items, empty_defaults)
paul@343 131
        finally:
paul@343 132
            self.release_lock(user)
paul@343 133
paul@648 134
    # Store object access.
paul@648 135
paul@329 136
    def _get_object(self, user, filename):
paul@329 137
paul@329 138
        """
paul@329 139
        Return the parsed object for the given 'user' having the given
paul@329 140
        'filename'.
paul@329 141
        """
paul@329 142
paul@329 143
        self.acquire_lock(user)
paul@329 144
        try:
paul@329 145
            f = open(filename, "rb")
paul@329 146
            try:
paul@329 147
                return parse_object(f, "utf-8")
paul@329 148
            finally:
paul@329 149
                f.close()
paul@329 150
        finally:
paul@329 151
            self.release_lock(user)
paul@329 152
paul@329 153
    def _set_object(self, user, filename, node):
paul@329 154
paul@329 155
        """
paul@329 156
        Set an object for the given 'user' having the given 'filename', using
paul@329 157
        'node' to define the object.
paul@329 158
        """
paul@329 159
paul@329 160
        self.acquire_lock(user)
paul@329 161
        try:
paul@329 162
            f = open(filename, "wb")
paul@329 163
            try:
paul@329 164
                to_stream(f, node)
paul@329 165
            finally:
paul@329 166
                f.close()
paul@329 167
                fix_permissions(filename)
paul@329 168
        finally:
paul@329 169
            self.release_lock(user)
paul@329 170
paul@329 171
        return True
paul@329 172
paul@329 173
    def _remove_object(self, filename):
paul@329 174
paul@329 175
        "Remove the object with the given 'filename'."
paul@329 176
paul@329 177
        try:
paul@329 178
            remove(filename)
paul@329 179
        except OSError:
paul@329 180
            return False
paul@329 181
paul@329 182
        return True
paul@329 183
paul@343 184
    def _remove_collection(self, filename):
paul@343 185
paul@343 186
        "Remove the collection with the given 'filename'."
paul@343 187
paul@343 188
        try:
paul@343 189
            rmdir(filename)
paul@343 190
        except OSError:
paul@343 191
            return False
paul@343 192
paul@343 193
        return True
paul@343 194
paul@670 195
    # User discovery.
paul@670 196
paul@670 197
    def get_users(self):
paul@670 198
paul@670 199
        "Return a list of users."
paul@670 200
paul@670 201
        return listdir(self.store_dir)
paul@670 202
paul@648 203
    # Event and event metadata access.
paul@648 204
paul@119 205
    def get_events(self, user):
paul@119 206
paul@119 207
        "Return a list of event identifiers."
paul@119 208
paul@138 209
        filename = self.get_object_in_store(user, "objects")
paul@119 210
        if not filename or not exists(filename):
paul@119 211
            return None
paul@119 212
paul@119 213
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@119 214
paul@648 215
    def get_all_events(self, user):
paul@648 216
paul@648 217
        "Return a set of (uid, recurrenceid) tuples for all events."
paul@648 218
paul@648 219
        uids = self.get_events(user)
paul@674 220
        if not uids:
paul@674 221
            return set()
paul@648 222
paul@648 223
        all_events = set()
paul@648 224
        for uid in uids:
paul@648 225
            all_events.add((uid, None))
paul@648 226
            all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
paul@648 227
paul@648 228
        return all_events
paul@648 229
paul@694 230
    def get_event_filename(self, user, uid, recurrenceid=None, dirname=None):
paul@648 231
paul@694 232
        """
paul@694 233
        Get the filename providing the event for the given 'user' with the given
paul@694 234
        'uid'. If the optional 'recurrenceid' is specified, a specific instance
paul@694 235
        or occurrence of an event is returned.
paul@648 236
paul@694 237
        Where 'dirname' is specified, the given directory name is used as the
paul@694 238
        base of the location within which any filename will reside.
paul@694 239
        """
paul@648 240
paul@694 241
        if recurrenceid:
paul@694 242
            return self.get_recurrence_filename(user, uid, recurrenceid, dirname)
paul@694 243
        else:
paul@694 244
            return self.get_complete_event_filename(user, uid, dirname)
paul@648 245
paul@343 246
    def get_event(self, user, uid, recurrenceid=None):
paul@343 247
paul@343 248
        """
paul@343 249
        Get the event for the given 'user' with the given 'uid'. If
paul@343 250
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 251
        occurrence of an event is returned.
paul@343 252
        """
paul@343 253
paul@694 254
        filename = self.get_event_filename(user, uid, recurrenceid)
paul@694 255
        if not filename or not exists(filename):
paul@694 256
            return None
paul@694 257
paul@694 258
        return filename and self._get_object(user, filename)
paul@694 259
paul@694 260
    def get_complete_event_filename(self, user, uid, dirname=None):
paul@694 261
paul@694 262
        """
paul@694 263
        Get the filename providing the event for the given 'user' with the given
paul@694 264
        'uid'. 
paul@694 265
paul@694 266
        Where 'dirname' is specified, the given directory name is used as the
paul@694 267
        base of the location within which any filename will reside.
paul@694 268
        """
paul@694 269
paul@694 270
        return self.get_object_in_store(user, dirname, "objects", uid)
paul@343 271
paul@343 272
    def get_complete_event(self, user, uid):
paul@50 273
paul@50 274
        "Get the event for the given 'user' with the given 'uid'."
paul@50 275
paul@694 276
        filename = self.get_complete_event_filename(user, uid)
paul@50 277
        if not filename or not exists(filename):
paul@50 278
            return None
paul@50 279
paul@694 280
        return filename and self._get_object(user, filename)
paul@50 281
paul@343 282
    def set_event(self, user, uid, recurrenceid, node):
paul@343 283
paul@343 284
        """
paul@343 285
        Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
paul@343 286
        if the latter is specified, a specific instance or occurrence of an
paul@343 287
        event is referenced), using the given 'node' description.
paul@343 288
        """
paul@343 289
paul@343 290
        if recurrenceid:
paul@343 291
            return self.set_recurrence(user, uid, recurrenceid, node)
paul@343 292
        else:
paul@343 293
            return self.set_complete_event(user, uid, node)
paul@343 294
paul@343 295
    def set_complete_event(self, user, uid, node):
paul@50 296
paul@50 297
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@50 298
paul@138 299
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 300
        if not filename:
paul@50 301
            return False
paul@50 302
paul@329 303
        return self._set_object(user, filename, node)
paul@15 304
paul@365 305
    def remove_event(self, user, uid, recurrenceid=None):
paul@234 306
paul@343 307
        """
paul@343 308
        Remove an event for 'user' having the given 'uid'. If the optional
paul@343 309
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@343 310
        event is removed.
paul@343 311
        """
paul@343 312
paul@343 313
        if recurrenceid:
paul@343 314
            return self.remove_recurrence(user, uid, recurrenceid)
paul@343 315
        else:
paul@343 316
            for recurrenceid in self.get_recurrences(user, uid) or []:
paul@343 317
                self.remove_recurrence(user, uid, recurrenceid)
paul@343 318
            return self.remove_complete_event(user, uid)
paul@343 319
paul@343 320
    def remove_complete_event(self, user, uid):
paul@343 321
paul@234 322
        "Remove an event for 'user' having the given 'uid'."
paul@234 323
paul@378 324
        self.remove_recurrences(user, uid)
paul@369 325
paul@234 326
        filename = self.get_object_in_store(user, "objects", uid)
paul@234 327
        if not filename:
paul@234 328
            return False
paul@234 329
paul@329 330
        return self._remove_object(filename)
paul@234 331
paul@334 332
    def get_recurrences(self, user, uid):
paul@334 333
paul@334 334
        """
paul@334 335
        Get additional event instances for an event of the given 'user' with the
paul@694 336
        indicated 'uid'. Both active and cancelled recurrences are returned.
paul@694 337
        """
paul@694 338
paul@694 339
        return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
paul@694 340
paul@694 341
    def get_active_recurrences(self, user, uid):
paul@694 342
paul@694 343
        """
paul@694 344
        Get additional event instances for an event of the given 'user' with the
paul@694 345
        indicated 'uid'. Cancelled recurrences are not returned.
paul@334 346
        """
paul@334 347
paul@334 348
        filename = self.get_object_in_store(user, "recurrences", uid)
paul@334 349
        if not filename or not exists(filename):
paul@347 350
            return []
paul@334 351
paul@334 352
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@334 353
paul@694 354
    def get_cancelled_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'. Only cancelled recurrences are returned.
paul@694 359
        """
paul@694 360
paul@694 361
        filename = self.get_object_in_store(user, "cancelled", "recurrences", uid)
paul@694 362
        if not filename or not exists(filename):
paul@694 363
            return []
paul@694 364
paul@694 365
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@694 366
paul@694 367
    def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None):
paul@694 368
paul@694 369
        """
paul@694 370
        For the event of the given 'user' with the given 'uid', return the
paul@694 371
        filename providing the recurrence with the given 'recurrenceid'.
paul@694 372
paul@694 373
        Where 'dirname' is specified, the given directory name is used as the
paul@694 374
        base of the location within which any filename will reside.
paul@694 375
        """
paul@694 376
paul@694 377
        return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid)
paul@694 378
paul@334 379
    def get_recurrence(self, user, uid, recurrenceid):
paul@334 380
paul@334 381
        """
paul@334 382
        For the event of the given 'user' with the given 'uid', return the
paul@334 383
        specific recurrence indicated by the 'recurrenceid'.
paul@334 384
        """
paul@334 385
paul@694 386
        filename = self.get_recurrence_filename(user, uid, recurrenceid)
paul@334 387
        if not filename or not exists(filename):
paul@334 388
            return None
paul@334 389
paul@694 390
        return filename and self._get_object(user, filename)
paul@334 391
paul@334 392
    def set_recurrence(self, user, uid, recurrenceid, node):
paul@334 393
paul@334 394
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@334 395
paul@334 396
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 397
        if not filename:
paul@334 398
            return False
paul@334 399
paul@334 400
        return self._set_object(user, filename, node)
paul@334 401
paul@334 402
    def remove_recurrence(self, user, uid, recurrenceid):
paul@334 403
paul@378 404
        """
paul@378 405
        Remove a special recurrence from an event stored by 'user' having the
paul@378 406
        given 'uid' and 'recurrenceid'.
paul@378 407
        """
paul@334 408
paul@378 409
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 410
        if not filename:
paul@334 411
            return False
paul@334 412
paul@334 413
        return self._remove_object(filename)
paul@334 414
paul@378 415
    def remove_recurrences(self, user, uid):
paul@378 416
paul@378 417
        """
paul@378 418
        Remove all recurrences for an event stored by 'user' having the given
paul@378 419
        'uid'.
paul@378 420
        """
paul@378 421
paul@378 422
        for recurrenceid in self.get_recurrences(user, uid):
paul@378 423
            self.remove_recurrence(user, uid, recurrenceid)
paul@378 424
paul@378 425
        recurrences = self.get_object_in_store(user, "recurrences", uid)
paul@378 426
        if recurrences:
paul@378 427
            return self._remove_collection(recurrences)
paul@378 428
paul@378 429
        return True
paul@378 430
paul@652 431
    # Free/busy period providers, upon extension of the free/busy records.
paul@652 432
paul@672 433
    def _get_freebusy_providers(self, user):
paul@672 434
paul@672 435
        """
paul@672 436
        Return the free/busy providers for the given 'user'.
paul@672 437
paul@672 438
        This function returns any stored datetime and a list of providers as a
paul@672 439
        2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
paul@672 440
        """
paul@672 441
paul@672 442
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@672 443
        if not filename or not exists(filename):
paul@672 444
            return None
paul@672 445
paul@672 446
        # Attempt to read providers, with a declaration of the datetime
paul@672 447
        # from which such providers are considered as still being active.
paul@672 448
paul@702 449
        t = self._get_table_atomic(user, filename, [(1, None)])
paul@672 450
        try:
paul@672 451
            dt_string = t[0][0]
paul@672 452
        except IndexError:
paul@672 453
            return None
paul@672 454
paul@672 455
        return dt_string, t[1:]
paul@672 456
paul@652 457
    def get_freebusy_providers(self, user, dt=None):
paul@652 458
paul@652 459
        """
paul@652 460
        Return a set of uncancelled events of the form (uid, recurrenceid)
paul@652 461
        providing free/busy details beyond the given datetime 'dt'.
paul@654 462
paul@654 463
        If 'dt' is not specified, all events previously found to provide
paul@654 464
        details will be returned. Otherwise, if 'dt' is earlier than the
paul@654 465
        datetime recorded for the known providers, None is returned, indicating
paul@654 466
        that the list of providers must be recomputed.
paul@672 467
paul@672 468
        This function returns a list of (uid, recurrenceid) tuples upon success.
paul@652 469
        """
paul@652 470
paul@672 471
        t = self._get_freebusy_providers(user)
paul@672 472
        if not t:
paul@672 473
            return None
paul@672 474
paul@672 475
        dt_string, t = t
paul@672 476
paul@672 477
        # If the requested datetime is earlier than the stated datetime, the
paul@672 478
        # providers will need to be recomputed.
paul@672 479
paul@672 480
        if dt:
paul@672 481
            providers_dt = get_datetime(dt_string)
paul@672 482
            if not providers_dt or providers_dt > dt:
paul@672 483
                return None
paul@672 484
paul@672 485
        # Otherwise, return the providers.
paul@672 486
paul@672 487
        return t[1:]
paul@672 488
paul@672 489
    def _set_freebusy_providers(self, user, dt_string, t):
paul@672 490
paul@672 491
        "Set the given provider timestamp 'dt_string' and table 't'."
paul@672 492
paul@652 493
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@672 494
        if not filename:
paul@672 495
            return False
paul@652 496
paul@672 497
        t.insert(0, (dt_string,))
paul@702 498
        self._set_table_atomic(user, filename, t, [(1, "")])
paul@672 499
        return True
paul@652 500
paul@654 501
    def set_freebusy_providers(self, user, dt, providers):
paul@654 502
paul@654 503
        """
paul@654 504
        Define the uncancelled events providing free/busy details beyond the
paul@654 505
        given datetime 'dt'.
paul@654 506
        """
paul@654 507
paul@672 508
        t = []
paul@654 509
paul@654 510
        for obj in providers:
paul@672 511
            t.append((obj.get_uid(), obj.get_recurrenceid()))
paul@672 512
paul@672 513
        return self._set_freebusy_providers(user, format_datetime(dt), t)
paul@654 514
paul@672 515
    def append_freebusy_provider(self, user, provider):
paul@672 516
paul@672 517
        "For the given 'user', append the free/busy 'provider'."
paul@672 518
paul@672 519
        t = self._get_freebusy_providers(user)
paul@672 520
        if not t:
paul@654 521
            return False
paul@654 522
paul@672 523
        dt_string, t = t
paul@672 524
        t.append((provider.get_uid(), provider.get_recurrenceid()))
paul@672 525
paul@672 526
        return self._set_freebusy_providers(user, dt_string, t)
paul@672 527
paul@672 528
    def remove_freebusy_provider(self, user, provider):
paul@672 529
paul@672 530
        "For the given 'user', remove the free/busy 'provider'."
paul@672 531
paul@672 532
        t = self._get_freebusy_providers(user)
paul@672 533
        if not t:
paul@672 534
            return False
paul@672 535
paul@672 536
        dt_string, t = t
paul@672 537
        try:
paul@672 538
            t.remove((provider.get_uid(), provider.get_recurrenceid()))
paul@672 539
        except ValueError:
paul@672 540
            return False
paul@672 541
paul@672 542
        return self._set_freebusy_providers(user, dt_string, t)
paul@654 543
paul@648 544
    # Free/busy period access.
paul@648 545
paul@702 546
    def get_freebusy(self, user, name=None, get_table=None):
paul@15 547
paul@15 548
        "Get free/busy details for the given 'user'."
paul@15 549
paul@702 550
        filename = self.get_object_in_store(user, name or "freebusy")
paul@15 551
        if not filename or not exists(filename):
paul@167 552
            return []
paul@112 553
        else:
paul@702 554
            return map(lambda t: FreeBusyPeriod(*t),
paul@702 555
                (get_table or self._get_table_atomic)(user, filename, [(4, None)]))
paul@702 556
paul@702 557
    def get_freebusy_for_update(self, user, name=None):
paul@2 558
paul@702 559
        """
paul@702 560
        Get free/busy details for the given 'user', locking the table. Dependent
paul@702 561
        code must release this lock regardless of it completing successfully.
paul@702 562
        """
paul@702 563
paul@702 564
        self.acquire_lock(user)
paul@702 565
        return self.get_freebusy(user, name, self._get_table)
paul@702 566
paul@702 567
    def get_freebusy_for_other(self, user, other, get_table=None):
paul@112 568
paul@112 569
        "For the given 'user', get free/busy details for the 'other' user."
paul@112 570
paul@112 571
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@167 572
        if not filename or not exists(filename):
paul@167 573
            return []
paul@112 574
        else:
paul@702 575
            return map(lambda t: FreeBusyPeriod(*t),
paul@702 576
                (get_table or self._get_table_atomic)(user, filename, [(4, None)]))
paul@702 577
paul@702 578
    def get_freebusy_for_other_for_update(self, user, other):
paul@2 579
paul@702 580
        """
paul@702 581
        For the given 'user', get free/busy details for the 'other' user,
paul@702 582
        locking the table. Dependent code must release this lock regardless of
paul@702 583
        it completing successfully.
paul@702 584
        """
paul@702 585
paul@702 586
        self.acquire_lock(user)
paul@702 587
        return self.get_freebusy_for_other(user, other, self._get_table)
paul@702 588
paul@702 589
    def set_freebusy(self, user, freebusy, name=None, set_table=None):
paul@15 590
paul@15 591
        "For the given 'user', set 'freebusy' details."
paul@15 592
paul@702 593
        filename = self.get_object_in_store(user, name or "freebusy")
paul@15 594
        if not filename:
paul@15 595
            return False
paul@15 596
paul@702 597
        (set_table or self._set_table_atomic)(user, filename,
paul@702 598
            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@15 599
        return True
paul@15 600
paul@702 601
    def set_freebusy_in_update(self, user, freebusy, name=None):
paul@702 602
paul@702 603
        "For the given 'user', set 'freebusy' details during a compound update."
paul@702 604
paul@702 605
        return self.set_freebusy(user, freebusy, name, self._set_table)
paul@702 606
paul@702 607
    def set_freebusy_for_other(self, user, freebusy, other, set_table=None):
paul@110 608
paul@110 609
        "For the given 'user', set 'freebusy' details for the 'other' user."
paul@110 610
paul@110 611
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@110 612
        if not filename:
paul@110 613
            return False
paul@110 614
paul@702 615
        (set_table or self._set_table_atomic)(user, filename,
paul@702 616
            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@112 617
        return True
paul@112 618
paul@702 619
    def set_freebusy_for_other_in_update(self, user, freebusy, other):
paul@702 620
paul@702 621
        """
paul@702 622
        For the given 'user', set 'freebusy' details for the 'other' user during
paul@702 623
        a compound update.
paul@702 624
        """
paul@702 625
paul@702 626
        return self.set_freebusy_for_other(user, freebusy, other, self._set_table)
paul@702 627
paul@702 628
    # Release methods.
paul@702 629
paul@702 630
    release_freebusy = release_lock
paul@702 631
paul@648 632
    # Object status details access.
paul@648 633
paul@142 634
    def _get_requests(self, user, queue):
paul@66 635
paul@142 636
        "Get requests for the given 'user' from the given 'queue'."
paul@66 637
paul@142 638
        filename = self.get_object_in_store(user, queue)
paul@81 639
        if not filename or not exists(filename):
paul@66 640
            return None
paul@66 641
paul@702 642
        return self._get_table_atomic(user, filename, [(1, None)])
paul@66 643
paul@142 644
    def get_requests(self, user):
paul@142 645
paul@142 646
        "Get requests for the given 'user'."
paul@142 647
paul@142 648
        return self._get_requests(user, "requests")
paul@142 649
paul@142 650
    def _set_requests(self, user, requests, queue):
paul@66 651
paul@142 652
        """
paul@142 653
        For the given 'user', set the list of queued 'requests' in the given
paul@142 654
        'queue'.
paul@142 655
        """
paul@142 656
paul@142 657
        filename = self.get_object_in_store(user, queue)
paul@66 658
        if not filename:
paul@66 659
            return False
paul@66 660
paul@303 661
        self.acquire_lock(user)
paul@66 662
        try:
paul@303 663
            f = open(filename, "w")
paul@303 664
            try:
paul@303 665
                for request in requests:
paul@343 666
                    print >>f, "\t".join([value or "" for value in request])
paul@303 667
            finally:
paul@303 668
                f.close()
paul@303 669
                fix_permissions(filename)
paul@66 670
        finally:
paul@303 671
            self.release_lock(user)
paul@66 672
paul@66 673
        return True
paul@66 674
paul@142 675
    def set_requests(self, user, requests):
paul@142 676
paul@142 677
        "For the given 'user', set the list of queued 'requests'."
paul@142 678
paul@142 679
        return self._set_requests(user, requests, "requests")
paul@142 680
paul@343 681
    def _set_request(self, user, uid, recurrenceid, queue):
paul@142 682
paul@343 683
        """
paul@343 684
        For the given 'user', set the queued 'uid' and 'recurrenceid' in the
paul@343 685
        given 'queue'.
paul@343 686
        """
paul@142 687
paul@142 688
        filename = self.get_object_in_store(user, queue)
paul@55 689
        if not filename:
paul@55 690
            return False
paul@55 691
paul@303 692
        self.acquire_lock(user)
paul@55 693
        try:
paul@303 694
            f = open(filename, "a")
paul@303 695
            try:
paul@343 696
                print >>f, "\t".join([uid, recurrenceid or ""])
paul@303 697
            finally:
paul@303 698
                f.close()
paul@303 699
                fix_permissions(filename)
paul@55 700
        finally:
paul@303 701
            self.release_lock(user)
paul@55 702
paul@55 703
        return True
paul@55 704
paul@343 705
    def set_request(self, user, uid, recurrenceid=None):
paul@142 706
paul@343 707
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 708
paul@343 709
        return self._set_request(user, uid, recurrenceid, "requests")
paul@142 710
paul@343 711
    def queue_request(self, user, uid, recurrenceid=None):
paul@142 712
paul@343 713
        """
paul@343 714
        Queue a request for 'user' having the given 'uid'. If the optional
paul@343 715
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 716
        or occurrence of an event.
paul@343 717
        """
paul@66 718
paul@81 719
        requests = self.get_requests(user) or []
paul@66 720
paul@343 721
        if (uid, recurrenceid) not in requests:
paul@343 722
            return self.set_request(user, uid, recurrenceid)
paul@66 723
paul@66 724
        return False
paul@66 725
paul@343 726
    def dequeue_request(self, user, uid, recurrenceid=None):
paul@105 727
paul@343 728
        """
paul@343 729
        Dequeue a request for 'user' having the given 'uid'. If the optional
paul@343 730
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 731
        or occurrence of an event.
paul@343 732
        """
paul@105 733
paul@105 734
        requests = self.get_requests(user) or []
paul@105 735
paul@105 736
        try:
paul@343 737
            requests.remove((uid, recurrenceid))
paul@105 738
            self.set_requests(user, requests)
paul@105 739
        except ValueError:
paul@105 740
            return False
paul@105 741
        else:
paul@105 742
            return True
paul@105 743
paul@343 744
    def cancel_event(self, user, uid, recurrenceid=None):
paul@142 745
paul@343 746
        """
paul@694 747
        Cancel an event for 'user' having the given 'uid'. If the optional
paul@694 748
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@694 749
        event is cancelled.
paul@343 750
        """
paul@142 751
paul@694 752
        filename = self.get_event_filename(user, uid, recurrenceid)
paul@694 753
        cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@142 754
paul@694 755
        if filename and cancelled_filename and exists(filename):
paul@694 756
            return self.move_object(filename, cancelled_filename)
paul@142 757
paul@142 758
        return False
paul@142 759
paul@30 760
class FilePublisher(FileBase):
paul@30 761
paul@30 762
    "A publisher of objects."
paul@30 763
paul@597 764
    def __init__(self, store_dir=None):
paul@597 765
        FileBase.__init__(self, store_dir or PUBLISH_DIR)
paul@30 766
paul@30 767
    def set_freebusy(self, user, freebusy):
paul@30 768
paul@30 769
        "For the given 'user', set 'freebusy' details."
paul@30 770
paul@52 771
        filename = self.get_object_in_store(user, "freebusy")
paul@30 772
        if not filename:
paul@30 773
            return False
paul@30 774
paul@30 775
        record = []
paul@30 776
        rwrite = record.append
paul@30 777
paul@30 778
        rwrite(("ORGANIZER", {}, user))
paul@30 779
        rwrite(("UID", {}, user))
paul@30 780
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 781
paul@458 782
        for fb in freebusy:
paul@458 783
            if not fb.transp or fb.transp == "OPAQUE":
paul@529 784
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
paul@563 785
                    map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
paul@30 786
paul@395 787
        f = open(filename, "wb")
paul@30 788
        try:
paul@30 789
            to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
paul@30 790
        finally:
paul@30 791
            f.close()
paul@103 792
            fix_permissions(filename)
paul@30 793
paul@30 794
        return True
paul@30 795
paul@2 796
# vim: tabstop=4 expandtab shiftwidth=4