imip-agent

Annotated imiptools/stores/file.py

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