~ubuntu-branches/ubuntu/utopic/calendarserver/utopic

« back to all changes in this revision

Viewing changes to txdav/caldav/datastore/index_file.py

  • Committer: Package Import Robot
  • Author(s): Rahul Amaram
  • Date: 2012-05-29 18:12:12 UTC
  • mfrom: (1.1.2)
  • Revision ID: package-import@ubuntu.com-20120529181212-mxjdfncopy6vou0f
Tags: 3.2+dfsg-1
* New upstream release
* Moved from using cdbs to dh sequencer
* Modified calenderserver init.d script based on /etc/init.d/skeleton script
* Removed ldapdirectory.patch as the OpenLDAP directory service has been 
  merged upstream
* Moved package to section "net" as calendarserver is more service than 
  library (Closes: #665859)
* Changed Architecture of calendarserver package to any as the package
  now includes compiled architecture dependent Python extensions
* Unowned files are no longer left on the system upon purging
  (Closes: #668731)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twistedcaldav.test.test_index -*-
 
2
##
 
3
# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
 
4
#
 
5
# Licensed under the Apache License, Version 2.0 (the "License");
 
6
# you may not use this file except in compliance with the License.
 
7
# You may obtain a copy of the License at
 
8
#
 
9
# http://www.apache.org/licenses/LICENSE-2.0
 
10
#
 
11
# Unless required by applicable law or agreed to in writing, software
 
12
# distributed under the License is distributed on an "AS IS" BASIS,
 
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
14
# See the License for the specific language governing permissions and
 
15
# limitations under the License.
 
16
##
 
17
 
 
18
"""
 
19
CalDAV Index.
 
20
 
 
21
This API is considered private to static.py and is therefore subject to
 
22
change.
 
23
"""
 
24
 
 
25
__all__ = [
 
26
    "db_basename",
 
27
    "ReservationError",
 
28
    "MemcachedUIDReserver",
 
29
    "Index",
 
30
    "IndexSchedule",
 
31
]
 
32
 
 
33
import datetime
 
34
import time
 
35
import hashlib
 
36
 
 
37
try:
 
38
    import sqlite3 as sqlite
 
39
except ImportError:
 
40
    from pysqlite2 import dbapi2 as sqlite
 
41
 
 
42
from twisted.internet.defer import maybeDeferred, succeed
 
43
 
 
44
from twext.python.log import Logger, LoggingMixIn
 
45
 
 
46
from txdav.common.icommondatastore import SyncTokenValidException,\
 
47
    ReservationError, IndexedSearchException
 
48
 
 
49
from twistedcaldav.dateops import pyCalendarTodatetime
 
50
from twistedcaldav.ical import Component
 
51
from twistedcaldav.query import calendarquery, calendarqueryfilter
 
52
from twistedcaldav.sql import AbstractSQLDatabase
 
53
from twistedcaldav.sql import db_prefix
 
54
from twistedcaldav.instance import InvalidOverriddenInstanceError
 
55
from twistedcaldav.config import config
 
56
from twistedcaldav.memcachepool import CachePoolUserMixIn
 
57
 
 
58
from pycalendar.datetime import PyCalendarDateTime
 
59
from pycalendar.duration import PyCalendarDuration
 
60
from pycalendar.timezone import PyCalendarTimezone
 
61
 
 
62
log = Logger()
 
63
 
 
64
db_basename = db_prefix + "sqlite"
 
65
schema_version = "10"
 
66
collection_types = {"Calendar": "Regular Calendar Collection", "iTIP": "iTIP Calendar Collection"}
 
67
 
 
68
icalfbtype_to_indexfbtype = {
 
69
    "FREE"            : 'F',
 
70
    "BUSY"            : 'B',
 
71
    "BUSY-UNAVAILABLE": 'U',
 
72
    "BUSY-TENTATIVE"  : 'T',
 
73
}
 
74
indexfbtype_to_icalfbtype = dict([(v, k) for k,v in icalfbtype_to_indexfbtype.iteritems()])
 
75
 
 
76
 
 
77
class AbstractCalendarIndex(AbstractSQLDatabase, LoggingMixIn):
 
78
    """
 
79
    Calendar collection index abstract base class that defines the apis for the index.
 
80
    This will be subclassed for the two types of index behaviour we need: one for
 
81
    regular calendar collections, one for schedule calendar collections.
 
82
    """
 
83
 
 
84
    def __init__(self, resource):
 
85
        """
 
86
        @param resource: the L{CalDAVResource} resource to
 
87
            index. C{resource} must be a calendar collection (ie.
 
88
            C{resource.isPseudoCalendarCollection()} returns C{True}.)
 
89
        """
 
90
        self.resource = resource
 
91
        db_filename = self.resource.fp.child(db_basename).path
 
92
        super(AbstractCalendarIndex, self).__init__(db_filename, False)
 
93
 
 
94
    def create(self):
 
95
        """
 
96
        Create the index and initialize it.
 
97
        """
 
98
        self._db()
 
99
 
 
100
    def reserveUID(self, uid):
 
101
        """
 
102
        Reserve a UID for this index's resource.
 
103
        @param uid: the UID to reserve
 
104
        @raise ReservationError: if C{uid} is already reserved
 
105
        """
 
106
        raise NotImplementedError
 
107
 
 
108
    def unreserveUID(self, uid):
 
109
        """
 
110
        Unreserve a UID for this index's resource.
 
111
        @param uid: the UID to reserve
 
112
        @raise ReservationError: if C{uid} is not reserved
 
113
        """
 
114
        raise NotImplementedError
 
115
 
 
116
    def isReservedUID(self, uid):
 
117
        """
 
118
        Check to see whether a UID is reserved.
 
119
        @param uid: the UID to check
 
120
        @return: True if C{uid} is reserved, False otherwise.
 
121
        """
 
122
        raise NotImplementedError
 
123
 
 
124
    def isAllowedUID(self, uid, *names):
 
125
        """
 
126
        Checks to see whether to allow an operation with adds the the specified
 
127
        UID is allowed to the index.  Specifically, the operation may not
 
128
        violate the constraint that UIDs must be unique, and the UID must not
 
129
        be reserved.
 
130
        @param uid: the UID to check
 
131
        @param names: the names of resources being replaced or deleted by the
 
132
            operation; UIDs associated with these resources are not checked.
 
133
        @return: True if the UID is not in the index and is not reserved,
 
134
            False otherwise.
 
135
        """
 
136
        raise NotImplementedError
 
137
 
 
138
    def resourceNamesForUID(self, uid):
 
139
        """
 
140
        Looks up the names of the resources with the given UID.
 
141
        @param uid: the UID of the resources to look up.
 
142
        @return: a list of resource names
 
143
        """
 
144
        names = self._db_values_for_sql("select NAME from RESOURCE where UID = :1", uid)
 
145
 
 
146
        #
 
147
        # Check that each name exists as a child of self.resource.  If not, the
 
148
        # resource record is stale.
 
149
        #
 
150
        resources = []
 
151
        for name in names:
 
152
            name_utf8 = name.encode("utf-8")
 
153
            if name is not None and self.resource.getChild(name_utf8) is None:
 
154
                # Clean up
 
155
                log.err("Stale resource record found for child %s with UID %s in %s" % (name, uid, self.resource))
 
156
                self._delete_from_db(name, uid, False)
 
157
                self._db_commit()
 
158
            else:
 
159
                resources.append(name_utf8)
 
160
 
 
161
        return resources
 
162
 
 
163
    def resourceNameForUID(self, uid):
 
164
        """
 
165
        Looks up the name of the resource with the given UID.
 
166
        @param uid: the UID of the resource to look up.
 
167
        @return: If the resource is found, its name; C{None} otherwise.
 
168
        """
 
169
        result = None
 
170
 
 
171
        for name in self.resourceNamesForUID(uid):
 
172
            assert result is None, "More than one resource with UID %s in calendar collection %r" % (uid, self)
 
173
            result = name
 
174
 
 
175
        return result
 
176
 
 
177
    def resourceUIDForName(self, name):
 
178
        """
 
179
        Looks up the UID of the resource with the given name.
 
180
        @param name: the name of the resource to look up.
 
181
        @return: If the resource is found, the UID of the resource; C{None}
 
182
            otherwise.
 
183
        """
 
184
        uid = self._db_value_for_sql("select UID from RESOURCE where NAME = :1", name)
 
185
 
 
186
        return uid
 
187
 
 
188
    def addResource(self, name, calendar, fast=False, reCreate=False):
 
189
        """
 
190
        Adding or updating an existing resource.
 
191
        To check for an update we attempt to get an existing UID
 
192
        for the resource name. If present, then the index entries for
 
193
        that UID are removed. After that the new index entries are added.
 
194
        @param name: the name of the resource to add.
 
195
        @param calendar: a L{Calendar} object representing the resource
 
196
            contents.
 
197
        @param fast: if C{True} do not do commit, otherwise do commit.
 
198
        """
 
199
        oldUID = self.resourceUIDForName(name)
 
200
        if oldUID is not None:
 
201
            self._delete_from_db(name, oldUID, False)
 
202
        self._add_to_db(name, calendar, reCreate=reCreate)
 
203
        if not fast:
 
204
            self._db_commit()
 
205
 
 
206
    def deleteResource(self, name):
 
207
        """
 
208
        Remove this resource from the index.
 
209
        @param name: the name of the resource to add.
 
210
        @param uid: the UID of the calendar component in the resource.
 
211
        """
 
212
        uid = self.resourceUIDForName(name)
 
213
        if uid is not None:
 
214
            self._delete_from_db(name, uid)
 
215
            self._db_commit()
 
216
 
 
217
    def resourceExists(self, name):
 
218
        """
 
219
        Determines whether the specified resource name exists in the index.
 
220
        @param name: the name of the resource to test
 
221
        @return: True if the resource exists, False if not
 
222
        """
 
223
        uid = self._db_value_for_sql("select UID from RESOURCE where NAME = :1", name)
 
224
        return uid is not None
 
225
 
 
226
    def resourcesExist(self, names):
 
227
        """
 
228
        Determines whether the specified resource name exists in the index.
 
229
        @param names: a C{list} containing the names of the resources to test
 
230
        @return: a C{list} of all names that exist
 
231
        """
 
232
        statement = "select NAME from RESOURCE where NAME in ("
 
233
        for ctr in (item[0] for item in enumerate(names)):
 
234
            if ctr != 0:
 
235
                statement += ", "
 
236
            statement += ":%s" % (ctr,)
 
237
        statement += ")"
 
238
        results = self._db_values_for_sql(statement, *names)
 
239
        return results
 
240
 
 
241
 
 
242
    def testAndUpdateIndex(self, minDate):
 
243
        # Find out if the index is expanded far enough
 
244
        names = self.notExpandedBeyond(minDate)
 
245
        # Actually expand recurrence max
 
246
        for name in names:
 
247
            self.log_info("Search falls outside range of index for %s %s" % (name, minDate))
 
248
            self.reExpandResource(name, minDate)
 
249
 
 
250
    def whatchanged(self, revision):
 
251
 
 
252
        results = [(name.encode("utf-8"), deleted) for name, deleted in self._db_execute("select NAME, DELETED from REVISIONS where REVISION > :1", revision)]
 
253
        results.sort(key=lambda x:x[1])
 
254
        
 
255
        changed = []
 
256
        deleted = []
 
257
        for name, wasdeleted in results:
 
258
            if name:
 
259
                if wasdeleted == 'Y':
 
260
                    if revision:
 
261
                        deleted.append(name)
 
262
                else:
 
263
                    changed.append(name)
 
264
            else:
 
265
                raise SyncTokenValidException
 
266
        
 
267
        return changed, deleted,
 
268
 
 
269
    def lastRevision(self):
 
270
        return self._db_value_for_sql(
 
271
            "select REVISION from REVISION_SEQUENCE"
 
272
        )
 
273
 
 
274
    def bumpRevision(self, fast=False):
 
275
        self._db_execute(
 
276
            """
 
277
            update REVISION_SEQUENCE set REVISION = REVISION + 1
 
278
            """,
 
279
        )
 
280
        self._db_commit()
 
281
        return self._db_value_for_sql(
 
282
            """
 
283
            select REVISION from REVISION_SEQUENCE
 
284
            """,
 
285
        )
 
286
 
 
287
    def indexedSearch(self, filter, useruid="", fbtype=False):
 
288
        """
 
289
        Finds resources matching the given qualifiers.
 
290
        @param filter: the L{Filter} for the calendar-query to execute.
 
291
        @return: an iterable of tuples for each resource matching the
 
292
            given C{qualifiers}. The tuples are C{(name, uid, type)}, where
 
293
            C{name} is the resource name, C{uid} is the resource UID, and
 
294
            C{type} is the resource iCalendar component type.
 
295
        """
 
296
 
 
297
        # Make sure we have a proper Filter element and get the partial SQL
 
298
        # statement to use.
 
299
        if isinstance(filter, calendarqueryfilter.Filter):
 
300
            if fbtype:
 
301
                # Lookup the useruid - try the empty (default) one if needed
 
302
                dbuseruid = self._db_value_for_sql(
 
303
                    "select PERUSERID from PERUSER where USERUID == :1",
 
304
                    useruid,
 
305
                )
 
306
            else:
 
307
                dbuseruid = ""
 
308
 
 
309
            qualifiers = calendarquery.sqlcalendarquery(filter, None, dbuseruid, fbtype)
 
310
            if qualifiers is not None:
 
311
                # Determine how far we need to extend the current expansion of
 
312
                # events. If we have an open-ended time-range we will expand one
 
313
                # year past the start. That should catch bounded recurrences - unbounded
 
314
                # will have been indexed with an "infinite" value always included.
 
315
                maxDate, isStartDate = filter.getmaxtimerange()
 
316
                if maxDate:
 
317
                    maxDate = maxDate.duplicate()
 
318
                    maxDate.setDateOnly(True)
 
319
                    if isStartDate:
 
320
                        maxDate += PyCalendarDuration(days=365)
 
321
                    self.testAndUpdateIndex(maxDate)
 
322
            else:
 
323
                # We cannot handle this filter in an indexed search
 
324
                raise IndexedSearchException()
 
325
 
 
326
        else:
 
327
            qualifiers = None
 
328
 
 
329
        # Perform the search
 
330
        if qualifiers is None:
 
331
            rowiter = self._db_execute("select NAME, UID, TYPE from RESOURCE")
 
332
        else:
 
333
            if fbtype:
 
334
                # For a free-busy time-range query we return all instances
 
335
                rowiter = self._db_execute(
 
336
                    "select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE, RESOURCE.ORGANIZER, TIMESPAN.FLOAT, TIMESPAN.START, TIMESPAN.END, TIMESPAN.FBTYPE, TIMESPAN.TRANSPARENT, TRANSPARENCY.TRANSPARENT" + 
 
337
                    qualifiers[0],
 
338
                    *qualifiers[1]
 
339
                )
 
340
            else:
 
341
                rowiter = self._db_execute("select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE" + qualifiers[0], *qualifiers[1])
 
342
 
 
343
        # Check result for missing resources
 
344
        results = []
 
345
        for row in rowiter:
 
346
            name = row[0]
 
347
            if self.resource.getChild(name.encode("utf-8")):
 
348
                if fbtype:
 
349
                    row = list(row)
 
350
                    if row[9]:
 
351
                        row[8] = row[9]
 
352
                    del row[9]
 
353
                results.append(row)
 
354
            else:
 
355
                log.err("Calendar resource %s is missing from %s. Removing from index."
 
356
                        % (name, self.resource))
 
357
                self.deleteResource(name)
 
358
 
 
359
        return results
 
360
 
 
361
    def bruteForceSearch(self):
 
362
        """
 
363
        List the whole index and tests for existence, updating the index
 
364
        @return: all resources in the index
 
365
        """
 
366
        # List all resources
 
367
        rowiter = self._db_execute("select NAME, UID, TYPE from RESOURCE")
 
368
 
 
369
        # Check result for missing resources:
 
370
 
 
371
        results = []
 
372
        for row in rowiter:
 
373
            name = row[0]
 
374
            if self.resource.getChild(name.encode("utf-8")):
 
375
                results.append(row)
 
376
            else:
 
377
                log.err("Calendar resource %s is missing from %s. Removing from index."
 
378
                        % (name, self.resource))
 
379
                self.deleteResource(name)
 
380
 
 
381
        return results
 
382
 
 
383
 
 
384
    def _db_version(self):
 
385
        """
 
386
        @return: the schema version assigned to this index.
 
387
        """
 
388
        return schema_version
 
389
 
 
390
    def _add_to_db(self, name, calendar, cursor=None, expand_until=None, reCreate=False):
 
391
        """
 
392
        Records the given calendar resource in the index with the given name.
 
393
        Resource names and UIDs must both be unique; only one resource name may
 
394
        be associated with any given UID and vice versa.
 
395
        NB This method does not commit the changes to the db - the caller
 
396
        MUST take care of that
 
397
        @param name: the name of the resource to add.
 
398
        @param calendar: a L{Calendar} object representing the resource
 
399
            contents.
 
400
        """
 
401
        raise NotImplementedError
 
402
 
 
403
    def _delete_from_db(self, name, uid, dorevision=True):
 
404
        """
 
405
        Deletes the specified entry from all dbs.
 
406
        @param name: the name of the resource to delete.
 
407
        @param uid: the uid of the resource to delete.
 
408
        """
 
409
        raise NotImplementedError
 
410
 
 
411
class CalendarIndex (AbstractCalendarIndex):
 
412
    """
 
413
    Calendar index - abstract class for indexer that indexes calendar objects in a collection.
 
414
    """
 
415
 
 
416
    def __init__(self, resource):
 
417
        """
 
418
        @param resource: the L{CalDAVResource} resource to
 
419
            index.
 
420
        """
 
421
        super(CalendarIndex, self).__init__(resource)
 
422
 
 
423
    def _db_init_data_tables_base(self, q, uidunique):
 
424
        """
 
425
        Initialise the underlying database tables.
 
426
        @param q:           a database cursor to use.
 
427
        """
 
428
        #
 
429
        # RESOURCE table is the primary index table
 
430
        #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
 
431
        #   UID: iCalendar UID (may or may not be unique)
 
432
        #   TYPE: iCalendar component type
 
433
        #   RECURRANCE_MAX: Highest date of recurrence expansion
 
434
        #   ORGANIZER: cu-address of the Organizer of the event
 
435
        #
 
436
        q.execute(
 
437
            """
 
438
            create table RESOURCE (
 
439
                RESOURCEID     integer primary key autoincrement,
 
440
                NAME           text unique,
 
441
                UID            text%s,
 
442
                TYPE           text,
 
443
                RECURRANCE_MAX date,
 
444
                ORGANIZER      text
 
445
            )
 
446
            """ % (" unique" if uidunique else "",)
 
447
        )
 
448
 
 
449
        #
 
450
        # TIMESPAN table tracks (expanded) time spans for resources
 
451
        #   NAME: Related resource (RESOURCE foreign key)
 
452
        #   FLOAT: 'Y' if start/end are floating, 'N' otherwise
 
453
        #   START: Start date
 
454
        #   END: End date
 
455
        #   FBTYPE: FBTYPE value:
 
456
        #     '?' - unknown
 
457
        #     'F' - free
 
458
        #     'B' - busy
 
459
        #     'U' - busy-unavailable
 
460
        #     'T' - busy-tentative
 
461
        #   TRANSPARENT: Y if transparent, N if opaque (default non-per-user value)
 
462
        #
 
463
        q.execute(
 
464
            """
 
465
            create table TIMESPAN (
 
466
                INSTANCEID   integer primary key autoincrement,
 
467
                RESOURCEID   integer,
 
468
                FLOAT        text(1),
 
469
                START        date,
 
470
                END          date,
 
471
                FBTYPE       text(1),
 
472
                TRANSPARENT  text(1)
 
473
            )
 
474
            """
 
475
        )
 
476
        q.execute(
 
477
            """
 
478
            create index STARTENDFLOAT on TIMESPAN (START, END, FLOAT)
 
479
            """
 
480
        )
 
481
 
 
482
        #
 
483
        # PERUSER table tracks per-user ids
 
484
        #   PERUSERID: autoincrement primary key
 
485
        #   UID: User ID used in calendar data
 
486
        #
 
487
        q.execute(
 
488
            """
 
489
            create table PERUSER (
 
490
                PERUSERID       integer primary key autoincrement,
 
491
                USERUID         text
 
492
            )
 
493
            """
 
494
        )
 
495
        q.execute(
 
496
            """
 
497
            create index PERUSER_UID on PERUSER (USERUID)
 
498
            """
 
499
        )
 
500
 
 
501
        #
 
502
        # TRANSPARENCY table tracks per-user per-instance transparency
 
503
        #   PERUSERID: user id key
 
504
        #   INSTANCEID: instance id key
 
505
        #   TRANSPARENT: Y if transparent, N if opaque
 
506
        #
 
507
        q.execute(
 
508
            """
 
509
            create table TRANSPARENCY (
 
510
                PERUSERID       integer,
 
511
                INSTANCEID      integer,
 
512
                TRANSPARENT     text(1)
 
513
            )
 
514
            """
 
515
        )
 
516
 
 
517
        #
 
518
        # REVISIONS table tracks changes
 
519
        #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
 
520
        #   REVISION: revision number
 
521
        #   WASDELETED: Y if revision deleted, N if added or changed
 
522
        #
 
523
        q.execute(
 
524
            """
 
525
            create table REVISION_SEQUENCE (
 
526
                REVISION        integer
 
527
            )
 
528
            """
 
529
        )
 
530
        q.execute(
 
531
            """
 
532
            insert into REVISION_SEQUENCE (REVISION) values (0)
 
533
            """
 
534
        )
 
535
        q.execute(
 
536
            """
 
537
            create table REVISIONS (
 
538
                NAME            text unique,
 
539
                REVISION        integer,
 
540
                DELETED         text(1)
 
541
            )
 
542
            """
 
543
        )
 
544
        q.execute(
 
545
            """
 
546
            create index REVISION on REVISIONS (REVISION)
 
547
            """
 
548
        )
 
549
 
 
550
        if uidunique:
 
551
            #
 
552
            # RESERVED table tracks reserved UIDs
 
553
            #   UID: The UID being reserved
 
554
            #   TIME: When the reservation was made
 
555
            #
 
556
            q.execute(
 
557
                """
 
558
                create table RESERVED (
 
559
                    UID  text unique,
 
560
                    TIME date
 
561
                )
 
562
                """
 
563
            )
 
564
 
 
565
        # Cascading triggers to help on delete
 
566
        q.execute(
 
567
            """
 
568
            create trigger resourceDelete after delete on RESOURCE
 
569
            for each row
 
570
            begin
 
571
                delete from TIMESPAN where TIMESPAN.RESOURCEID = OLD.RESOURCEID;
 
572
            end
 
573
            """
 
574
        )
 
575
        q.execute(
 
576
            """
 
577
            create trigger timespanDelete after delete on TIMESPAN
 
578
            for each row
 
579
            begin
 
580
                delete from TRANSPARENCY where INSTANCEID = OLD.INSTANCEID;
 
581
            end
 
582
            """
 
583
        )
 
584
        
 
585
    def _db_can_upgrade(self, old_version):
 
586
        """
 
587
        Can we do an in-place upgrade
 
588
        """
 
589
        
 
590
        # v10 is a big change - no upgrade possible
 
591
        return False
 
592
 
 
593
    def _db_upgrade_data_tables(self, q, old_version):
 
594
        """
 
595
        Upgrade the data from an older version of the DB.
 
596
        """
 
597
 
 
598
        # v10 is a big change - no upgrade possible
 
599
        pass
 
600
 
 
601
    def notExpandedBeyond(self, minDate):
 
602
        """
 
603
        Gives all resources which have not been expanded beyond a given date
 
604
        in the index
 
605
        """
 
606
        return self._db_values_for_sql("select NAME from RESOURCE where RECURRANCE_MAX < :1", pyCalendarTodatetime(minDate))
 
607
 
 
608
    def reExpandResource(self, name, expand_until):
 
609
        """
 
610
        Given a resource name, remove it from the database and re-add it
 
611
        with a longer expansion.
 
612
        """
 
613
        calendar = self.resource.getChild(name).iCalendar()
 
614
        self._add_to_db(name, calendar, expand_until=expand_until, reCreate=True)
 
615
        self._db_commit()
 
616
 
 
617
    def _add_to_db(self, name, calendar, cursor = None, expand_until=None, reCreate=False):
 
618
        """
 
619
        Records the given calendar resource in the index with the given name.
 
620
        Resource names and UIDs must both be unique; only one resource name may
 
621
        be associated with any given UID and vice versa.
 
622
        NB This method does not commit the changes to the db - the caller
 
623
        MUST take care of that
 
624
        @param name: the name of the resource to add.
 
625
        @param calendar: a L{Calendar} object representing the resource
 
626
            contents.
 
627
        """
 
628
        uid = calendar.resourceUID()
 
629
        organizer = calendar.getOrganizer()
 
630
        if not organizer:
 
631
            organizer = ""
 
632
 
 
633
        # Decide how far to expand based on the component
 
634
        doInstanceIndexing = False
 
635
        master = calendar.masterComponent()
 
636
        if master is None or not calendar.isRecurring():
 
637
            # When there is no master we have a set of overridden components - index them all.
 
638
            # When there is one instance - index it.
 
639
            expand = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
 
640
            doInstanceIndexing = True
 
641
        else:
 
642
            # If migrating or re-creating or config option for delayed indexing is off, always index
 
643
            if reCreate or not config.FreeBusyIndexDelayedExpand:
 
644
                doInstanceIndexing = True
 
645
 
 
646
            # Duration into the future through which recurrences are expanded in the index
 
647
            # by default.  This is a caching parameter which affects the size of the index;
 
648
            # it does not affect search results beyond this period, but it may affect
 
649
            # performance of such a search.
 
650
            expand = (PyCalendarDateTime.getToday() +
 
651
                      PyCalendarDuration(days=config.FreeBusyIndexExpandAheadDays))
 
652
 
 
653
            if expand_until and expand_until > expand:
 
654
                expand = expand_until
 
655
 
 
656
            # Maximum duration into the future through which recurrences are expanded in the
 
657
            # index.  This is a caching parameter which affects the size of the index; it
 
658
            # does not affect search results beyond this period, but it may affect
 
659
            # performance of such a search.
 
660
            #
 
661
            # When a search is performed on a time span that goes beyond that which is
 
662
            # expanded in the index, we have to open each resource which may have data in
 
663
            # that time period.  In order to avoid doing that multiple times, we want to
 
664
            # cache those results.  However, we don't necessarily want to cache all
 
665
            # occurrences into some obscenely far-in-the-future date, so we cap the caching
 
666
            # period.  Searches beyond this period will always be relatively expensive for
 
667
            # resources with occurrences beyond this period.
 
668
            if expand > (PyCalendarDateTime.getToday() +
 
669
                         PyCalendarDuration(days=config.FreeBusyIndexExpandMaxDays)):
 
670
                raise IndexedSearchException()
 
671
 
 
672
        # Always do recurrence expansion even if we do not intend to index - we need this to double-check the
 
673
        # validity of the iCalendar recurrence data.
 
674
        try:
 
675
            instances = calendar.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)
 
676
            recurrenceLimit = instances.limit
 
677
        except InvalidOverriddenInstanceError, e:
 
678
            log.err("Invalid instance %s when indexing %s in %s" % (e.rid, name, self.resource,))
 
679
            raise
 
680
 
 
681
        # Now coerce indexing to off if needed 
 
682
        if not doInstanceIndexing:
 
683
            instances = None
 
684
            recurrenceLimit = PyCalendarDateTime(1900, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
 
685
            
 
686
        self._delete_from_db(name, uid, False)
 
687
 
 
688
        # Add RESOURCE item
 
689
        self._db_execute(
 
690
            """
 
691
            insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX, ORGANIZER)
 
692
            values (:1, :2, :3, :4, :5)
 
693
            """, name, uid, calendar.resourceType(), pyCalendarTodatetime(recurrenceLimit) if recurrenceLimit else None, organizer
 
694
        )
 
695
        resourceid = self.lastrowid
 
696
 
 
697
        # Get a set of all referenced per-user UIDs and map those to entries already
 
698
        # in the DB and add new ones as needed
 
699
        useruids = calendar.allPerUserUIDs()
 
700
        useruids.add("")
 
701
        useruidmap = {}
 
702
        for useruid in useruids:
 
703
            peruserid = self._db_value_for_sql(
 
704
                "select PERUSERID from PERUSER where USERUID = :1",
 
705
                useruid
 
706
            )
 
707
            if peruserid is None:
 
708
                self._db_execute(
 
709
                    """
 
710
                    insert into PERUSER (USERUID)
 
711
                    values (:1)
 
712
                    """, useruid
 
713
                )
 
714
                peruserid = self.lastrowid
 
715
            useruidmap[useruid] = peruserid
 
716
            
 
717
        if doInstanceIndexing:
 
718
            for key in instances:
 
719
                instance = instances[key]
 
720
                start = instance.start
 
721
                end = instance.end
 
722
                float = 'Y' if instance.start.floating() else 'N'
 
723
                transp = 'T' if instance.component.propertyValue("TRANSP") == "TRANSPARENT" else 'F'
 
724
                self._db_execute(
 
725
                    """
 
726
                    insert into TIMESPAN (RESOURCEID, FLOAT, START, END, FBTYPE, TRANSPARENT)
 
727
                    values (:1, :2, :3, :4, :5, :6)
 
728
                    """,
 
729
                    resourceid,
 
730
                    float,
 
731
                    pyCalendarTodatetime(start),
 
732
                    pyCalendarTodatetime(end),
 
733
                    icalfbtype_to_indexfbtype.get(instance.component.getFBType(), 'F'),
 
734
                    transp
 
735
                )
 
736
                instanceid = self.lastrowid
 
737
                peruserdata = calendar.perUserTransparency(instance.rid)
 
738
                for useruid, transp in peruserdata:
 
739
                    peruserid = useruidmap[useruid]
 
740
                    self._db_execute(
 
741
                        """
 
742
                        insert into TRANSPARENCY (PERUSERID, INSTANCEID, TRANSPARENT)
 
743
                        values (:1, :2, :3)
 
744
                        """, peruserid, instanceid, 'T' if transp else 'F'
 
745
                    )
 
746
                        
 
747
    
 
748
            # Special - for unbounded recurrence we insert a value for "infinity"
 
749
            # that will allow an open-ended time-range to always match it.
 
750
            if calendar.isRecurringUnbounded():
 
751
                start = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
 
752
                end = PyCalendarDateTime(2100, 1, 1, 1, 0, 0, tzid=PyCalendarTimezone(utc=True))
 
753
                float = 'N'
 
754
                self._db_execute(
 
755
                    """
 
756
                    insert into TIMESPAN (RESOURCEID, FLOAT, START, END, FBTYPE, TRANSPARENT)
 
757
                    values (:1, :2, :3, :4, :5, :6)
 
758
                    """, resourceid, float, pyCalendarTodatetime(start), pyCalendarTodatetime(end), '?', '?'
 
759
                )
 
760
                instanceid = self.lastrowid
 
761
                peruserdata = calendar.perUserTransparency(None)
 
762
                for useruid, transp in peruserdata:
 
763
                    peruserid = useruidmap[useruid]
 
764
                    self._db_execute(
 
765
                        """
 
766
                        insert into TRANSPARENCY (PERUSERID, INSTANCEID, TRANSPARENT)
 
767
                        values (:1, :2, :3)
 
768
                        """, peruserid, instanceid, 'T' if transp else 'F'
 
769
                    )
 
770
            
 
771
        self._db_execute(
 
772
            """
 
773
            insert or replace into REVISIONS (NAME, REVISION, DELETED)
 
774
            values (:1, :2, :3)
 
775
            """, name, self.bumpRevision(fast=True), 'N',
 
776
        )
 
777
 
 
778
    def _delete_from_db(self, name, uid, dorevision=True):
 
779
        """
 
780
        Deletes the specified entry from all dbs.
 
781
        @param name: the name of the resource to delete.
 
782
        @param uid: the uid of the resource to delete.
 
783
        """
 
784
        self._db_execute("delete from RESOURCE where NAME = :1", name)
 
785
        if dorevision:
 
786
            self._db_execute(
 
787
                """
 
788
                update REVISIONS SET REVISION = :1, DELETED = :2
 
789
                where NAME = :3
 
790
                """, self.bumpRevision(fast=True), 'Y', name
 
791
            )
 
792
 
 
793
 
 
794
def wrapInDeferred(f):
 
795
    def _(*args, **kwargs):
 
796
        return maybeDeferred(f, *args, **kwargs)
 
797
 
 
798
    return _
 
799
 
 
800
 
 
801
class MemcachedUIDReserver(CachePoolUserMixIn, LoggingMixIn):
 
802
    def __init__(self, index, cachePool=None):
 
803
        self.index = index
 
804
        self._cachePool = cachePool
 
805
 
 
806
    def _key(self, uid):
 
807
        return 'reservation:%s' % (
 
808
            hashlib.md5('%s:%s' % (uid,
 
809
                                   self.index.resource.fp.path)).hexdigest())
 
810
 
 
811
    def reserveUID(self, uid):
 
812
        uid = uid.encode('utf-8')
 
813
        self.log_debug("Reserving UID %r @ %r" % (
 
814
                uid,
 
815
                self.index.resource.fp.path))
 
816
 
 
817
        def _handleFalse(result):
 
818
            if result is False:
 
819
                raise ReservationError(
 
820
                    "UID %s already reserved for calendar collection %s."
 
821
                    % (uid, self.index.resource)
 
822
                    )
 
823
 
 
824
        d = self.getCachePool().add(self._key(uid),
 
825
                                    'reserved',
 
826
                                    expireTime=config.UIDReservationTimeOut)
 
827
        d.addCallback(_handleFalse)
 
828
        return d
 
829
 
 
830
 
 
831
    def unreserveUID(self, uid):
 
832
        uid = uid.encode('utf-8')
 
833
        self.log_debug("Unreserving UID %r @ %r" % (
 
834
                uid,
 
835
                self.index.resource.fp.path))
 
836
 
 
837
        def _handleFalse(result):
 
838
            if result is False:
 
839
                raise ReservationError(
 
840
                    "UID %s is not reserved for calendar collection %s."
 
841
                    % (uid, self.index.resource)
 
842
                    )
 
843
 
 
844
        d =self.getCachePool().delete(self._key(uid))
 
845
        d.addCallback(_handleFalse)
 
846
        return d
 
847
 
 
848
 
 
849
    def isReservedUID(self, uid):
 
850
        uid = uid.encode('utf-8')
 
851
        self.log_debug("Is reserved UID %r @ %r" % (
 
852
                uid,
 
853
                self.index.resource.fp.path))
 
854
 
 
855
        def _checkValue((flags, value)):
 
856
            if value is None:
 
857
                return False
 
858
            else:
 
859
                return True
 
860
 
 
861
        d = self.getCachePool().get(self._key(uid))
 
862
        d.addCallback(_checkValue)
 
863
        return d
 
864
 
 
865
 
 
866
 
 
867
class SQLUIDReserver(object):
 
868
    def __init__(self, index):
 
869
        self.index = index
 
870
 
 
871
    @wrapInDeferred
 
872
    def reserveUID(self, uid):
 
873
        """
 
874
        Reserve a UID for this index's resource.
 
875
        @param uid: the UID to reserve
 
876
        @raise ReservationError: if C{uid} is already reserved
 
877
        """
 
878
 
 
879
        try:
 
880
            self.index._db_execute("insert into RESERVED (UID, TIME) values (:1, :2)", uid, datetime.datetime.now())
 
881
            self.index._db_commit()
 
882
        except sqlite.IntegrityError:
 
883
            self.index._db_rollback()
 
884
            raise ReservationError(
 
885
                "UID %s already reserved for calendar collection %s."
 
886
                % (uid, self.index.resource)
 
887
            )
 
888
        except sqlite.Error, e:
 
889
            log.err("Unable to reserve UID: %s", (e,))
 
890
            self.index._db_rollback()
 
891
            raise
 
892
 
 
893
    def unreserveUID(self, uid):
 
894
        """
 
895
        Unreserve a UID for this index's resource.
 
896
        @param uid: the UID to reserve
 
897
        @raise ReservationError: if C{uid} is not reserved
 
898
        """
 
899
 
 
900
        def _cb(result):
 
901
            if result == False:
 
902
                raise ReservationError(
 
903
                    "UID %s is not reserved for calendar collection %s."
 
904
                    % (uid, self.index.resource)
 
905
                    )
 
906
            else:
 
907
                try:
 
908
                    self.index._db_execute(
 
909
                        "delete from RESERVED where UID = :1", uid)
 
910
                    self.index._db_commit()
 
911
                except sqlite.Error, e:
 
912
                    log.err("Unable to unreserve UID: %s", (e,))
 
913
                    self.index._db_rollback()
 
914
                    raise
 
915
 
 
916
        d = self.isReservedUID(uid)
 
917
        d.addCallback(_cb)
 
918
        return d
 
919
 
 
920
 
 
921
    @wrapInDeferred
 
922
    def isReservedUID(self, uid):
 
923
        """
 
924
        Check to see whether a UID is reserved.
 
925
        @param uid: the UID to check
 
926
        @return: True if C{uid} is reserved, False otherwise.
 
927
        """
 
928
 
 
929
        rowiter = self.index._db_execute("select UID, TIME from RESERVED where UID = :1", uid)
 
930
        for uid, attime in rowiter:
 
931
            # Double check that the time is within a reasonable period of now
 
932
            # otherwise we probably have a stale reservation
 
933
            tm = time.strptime(attime[:19], "%Y-%m-%d %H:%M:%S")
 
934
            dt = datetime.datetime(year=tm.tm_year, month=tm.tm_mon, day=tm.tm_mday, hour=tm.tm_hour, minute=tm.tm_min, second = tm.tm_sec)
 
935
            if datetime.datetime.now() - dt > datetime.timedelta(seconds=config.UIDReservationTimeOut):
 
936
                try:
 
937
                    self.index._db_execute("delete from RESERVED where UID = :1", uid)
 
938
                    self.index._db_commit()
 
939
                except sqlite.Error, e:
 
940
                    log.err("Unable to unreserve UID: %s", (e,))
 
941
                    self.index._db_rollback()
 
942
                    raise
 
943
                return False
 
944
            else:
 
945
                return True
 
946
 
 
947
        return False
 
948
 
 
949
 
 
950
 
 
951
class Index (CalendarIndex):
 
952
    """
 
953
    Calendar collection index - regular collection that enforces CalDAV UID uniqueness requirement.
 
954
    """
 
955
 
 
956
    def __init__(self, resource):
 
957
        """
 
958
        @param resource: the L{CalDAVResource} resource to
 
959
            index. C{resource} must be a calendar collection (i.e.
 
960
            C{resource.isPseudoCalendarCollection()} returns C{True}.)
 
961
        """
 
962
        assert resource.isCalendarCollection(), "non-calendar collection resource %s has no index." % (resource,)
 
963
        super(Index, self).__init__(resource)
 
964
 
 
965
        if (
 
966
            hasattr(config, "Memcached") and
 
967
            config.Memcached.Pools.Default.ClientEnabled
 
968
        ):
 
969
            self.reserver = MemcachedUIDReserver(self)
 
970
        else:
 
971
            self.reserver = SQLUIDReserver(self)
 
972
 
 
973
    #
 
974
    # A dict of sets. The dict keys are calendar collection paths,
 
975
    # and the sets contains reserved UIDs for each path.
 
976
    #
 
977
 
 
978
    def reserveUID(self, uid):
 
979
        return self.reserver.reserveUID(uid)
 
980
 
 
981
 
 
982
    def unreserveUID(self, uid):
 
983
        return self.reserver.unreserveUID(uid)
 
984
 
 
985
 
 
986
    def isReservedUID(self, uid):
 
987
        return self.reserver.isReservedUID(uid)
 
988
 
 
989
 
 
990
    def isAllowedUID(self, uid, *names):
 
991
        """
 
992
        Checks to see whether to allow an operation which would add the
 
993
        specified UID to the index.  Specifically, the operation may not
 
994
        violate the constraint that UIDs must be unique.
 
995
        @param uid: the UID to check
 
996
        @param names: the names of resources being replaced or deleted by the
 
997
            operation; UIDs associated with these resources are not checked.
 
998
        @return: True if the UID is not in the index and is not reserved,
 
999
            False otherwise.
 
1000
        """
 
1001
        rname = self.resourceNameForUID(uid)
 
1002
        return (rname is None or rname in names)
 
1003
 
 
1004
    def _db_type(self):
 
1005
        """
 
1006
        @return: the collection type assigned to this index.
 
1007
        """
 
1008
        return collection_types["Calendar"]
 
1009
 
 
1010
    def _db_init_data_tables(self, q):
 
1011
        """
 
1012
        Initialise the underlying database tables.
 
1013
        @param q:           a database cursor to use.
 
1014
        """
 
1015
 
 
1016
        # Create database where the RESOURCE table has unique UID column.
 
1017
        self._db_init_data_tables_base(q, True)
 
1018
 
 
1019
    def _db_recreate(self, do_commit=True):
 
1020
        """
 
1021
        Re-create the database tables from existing calendar data.
 
1022
        """
 
1023
 
 
1024
        #
 
1025
        # Populate the DB with data from already existing resources.
 
1026
        # This allows for index recovery if the DB file gets
 
1027
        # deleted.
 
1028
        #
 
1029
        fp = self.resource.fp
 
1030
        for name in fp.listdir():
 
1031
            if name.startswith("."):
 
1032
                continue
 
1033
 
 
1034
            try:
 
1035
                stream = fp.child(name).open()
 
1036
            except (IOError, OSError), e:
 
1037
                log.err("Unable to open resource %s: %s" % (name, e))
 
1038
                continue
 
1039
 
 
1040
            # FIXME: This is blocking I/O
 
1041
            try:
 
1042
                calendar = Component.fromStream(stream)
 
1043
                calendar.validCalendarData()
 
1044
                calendar.validCalendarForCalDAV(methodAllowed=False)
 
1045
            except ValueError:
 
1046
                log.err("Non-calendar resource: %s" % (name,))
 
1047
            else:
 
1048
                #log.msg("Indexing resource: %s" % (name,))
 
1049
                self.addResource(name, calendar, True, reCreate=True)
 
1050
            finally:
 
1051
                stream.close()
 
1052
 
 
1053
        # Do commit outside of the loop for better performance
 
1054
        if do_commit:
 
1055
            self._db_commit()
 
1056
 
 
1057
class IndexSchedule (CalendarIndex):
 
1058
    """
 
1059
    Schedule collection index - does not require UID uniqueness.
 
1060
    """
 
1061
 
 
1062
    def reserveUID(self, uid): #@UnusedVariable
 
1063
        """
 
1064
        Reserve a UID for this index's resource.
 
1065
        @param uid: the UID to reserve
 
1066
        @raise ReservationError: if C{uid} is already reserved
 
1067
        """
 
1068
 
 
1069
        # iTIP does not require unique UIDs
 
1070
        return succeed(None)
 
1071
 
 
1072
    def unreserveUID(self, uid): #@UnusedVariable
 
1073
        """
 
1074
        Unreserve a UID for this index's resource.
 
1075
        @param uid: the UID to reserve
 
1076
        @raise ReservationError: if C{uid} is not reserved
 
1077
        """
 
1078
 
 
1079
        # iTIP does not require unique UIDs
 
1080
        return succeed(None)
 
1081
 
 
1082
    def isReservedUID(self, uid): #@UnusedVariable
 
1083
        """
 
1084
        Check to see whether a UID is reserved.
 
1085
        @param uid: the UID to check
 
1086
        @return: True if C{uid} is reserved, False otherwise.
 
1087
        """
 
1088
 
 
1089
        # iTIP does not require unique UIDs
 
1090
        return succeed(False)
 
1091
 
 
1092
    def isAllowedUID(self, uid, *names): #@UnusedVariable
 
1093
        """
 
1094
        Checks to see whether to allow an operation with adds the the specified
 
1095
        UID is allowed to the index.  Specifically, the operation may not
 
1096
        violate the constraint that UIDs must be unique, and the UID must not
 
1097
        be reserved.
 
1098
        @param uid: the UID to check
 
1099
        @param names: the names of resources being replaced or deleted by the
 
1100
            operation; UIDs associated with these resources are not checked.
 
1101
        @return: True if the UID is not in the index and is not reserved,
 
1102
            False otherwise.
 
1103
        """
 
1104
 
 
1105
        # iTIP does not require unique UIDs
 
1106
        return True
 
1107
 
 
1108
    def _db_type(self):
 
1109
        """
 
1110
        @return: the collection type assigned to this index.
 
1111
        """
 
1112
        return collection_types["iTIP"]
 
1113
 
 
1114
    def _db_init_data_tables(self, q):
 
1115
        """
 
1116
        Initialise the underlying database tables.
 
1117
        @param q:           a database cursor to use.
 
1118
        """
 
1119
 
 
1120
        # Create database where the RESOURCE table has a UID column that is not unique.
 
1121
        self._db_init_data_tables_base(q, False)
 
1122
 
 
1123
    def _db_recreate(self, do_commit=True):
 
1124
        """
 
1125
        Re-create the database tables from existing calendar data.
 
1126
        """
 
1127
 
 
1128
        #
 
1129
        # Populate the DB with data from already existing resources.
 
1130
        # This allows for index recovery if the DB file gets
 
1131
        # deleted.
 
1132
        #
 
1133
        fp = self.resource.fp
 
1134
        for name in fp.listdir():
 
1135
            if name.startswith("."):
 
1136
                continue
 
1137
 
 
1138
            try:
 
1139
                stream = fp.child(name).open()
 
1140
            except (IOError, OSError), e:
 
1141
                log.err("Unable to open resource %s: %s" % (name, e))
 
1142
                continue
 
1143
 
 
1144
            # FIXME: This is blocking I/O
 
1145
            try:
 
1146
                calendar = Component.fromStream(stream)
 
1147
                calendar.validCalendarData()
 
1148
                calendar.validCalendarForCalDAV(methodAllowed=True)
 
1149
            except ValueError:
 
1150
                log.err("Non-calendar resource: %s" % (name,))
 
1151
            else:
 
1152
                #log.msg("Indexing resource: %s" % (name,))
 
1153
                self.addResource(name, calendar, True, reCreate=True)
 
1154
            finally:
 
1155
                stream.close()
 
1156
 
 
1157
        # Do commit outside of the loop for better performance
 
1158
        if do_commit:
 
1159
            self._db_commit()