1
# -*- test-case-name: twistedcaldav.test.test_index -*-
3
# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
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
9
# http://www.apache.org/licenses/LICENSE-2.0
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.
21
This API is considered private to static.py and is therefore subject to
28
"MemcachedUIDReserver",
38
import sqlite3 as sqlite
40
from pysqlite2 import dbapi2 as sqlite
42
from twisted.internet.defer import maybeDeferred, succeed
44
from twext.python.log import Logger, LoggingMixIn
46
from txdav.common.icommondatastore import SyncTokenValidException,\
47
ReservationError, IndexedSearchException
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
58
from pycalendar.datetime import PyCalendarDateTime
59
from pycalendar.duration import PyCalendarDuration
60
from pycalendar.timezone import PyCalendarTimezone
64
db_basename = db_prefix + "sqlite"
66
collection_types = {"Calendar": "Regular Calendar Collection", "iTIP": "iTIP Calendar Collection"}
68
icalfbtype_to_indexfbtype = {
71
"BUSY-UNAVAILABLE": 'U',
72
"BUSY-TENTATIVE" : 'T',
74
indexfbtype_to_icalfbtype = dict([(v, k) for k,v in icalfbtype_to_indexfbtype.iteritems()])
77
class AbstractCalendarIndex(AbstractSQLDatabase, LoggingMixIn):
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.
84
def __init__(self, resource):
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}.)
90
self.resource = resource
91
db_filename = self.resource.fp.child(db_basename).path
92
super(AbstractCalendarIndex, self).__init__(db_filename, False)
96
Create the index and initialize it.
100
def reserveUID(self, uid):
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
106
raise NotImplementedError
108
def unreserveUID(self, uid):
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
114
raise NotImplementedError
116
def isReservedUID(self, uid):
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.
122
raise NotImplementedError
124
def isAllowedUID(self, uid, *names):
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
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,
136
raise NotImplementedError
138
def resourceNamesForUID(self, uid):
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
144
names = self._db_values_for_sql("select NAME from RESOURCE where UID = :1", uid)
147
# Check that each name exists as a child of self.resource. If not, the
148
# resource record is stale.
152
name_utf8 = name.encode("utf-8")
153
if name is not None and self.resource.getChild(name_utf8) is None:
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)
159
resources.append(name_utf8)
163
def resourceNameForUID(self, uid):
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.
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)
177
def resourceUIDForName(self, name):
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}
184
uid = self._db_value_for_sql("select UID from RESOURCE where NAME = :1", name)
188
def addResource(self, name, calendar, fast=False, reCreate=False):
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
197
@param fast: if C{True} do not do commit, otherwise do commit.
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)
206
def deleteResource(self, name):
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.
212
uid = self.resourceUIDForName(name)
214
self._delete_from_db(name, uid)
217
def resourceExists(self, name):
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
223
uid = self._db_value_for_sql("select UID from RESOURCE where NAME = :1", name)
224
return uid is not None
226
def resourcesExist(self, names):
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
232
statement = "select NAME from RESOURCE where NAME in ("
233
for ctr in (item[0] for item in enumerate(names)):
236
statement += ":%s" % (ctr,)
238
results = self._db_values_for_sql(statement, *names)
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
247
self.log_info("Search falls outside range of index for %s %s" % (name, minDate))
248
self.reExpandResource(name, minDate)
250
def whatchanged(self, revision):
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])
257
for name, wasdeleted in results:
259
if wasdeleted == 'Y':
265
raise SyncTokenValidException
267
return changed, deleted,
269
def lastRevision(self):
270
return self._db_value_for_sql(
271
"select REVISION from REVISION_SEQUENCE"
274
def bumpRevision(self, fast=False):
277
update REVISION_SEQUENCE set REVISION = REVISION + 1
281
return self._db_value_for_sql(
283
select REVISION from REVISION_SEQUENCE
287
def indexedSearch(self, filter, useruid="", fbtype=False):
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.
297
# Make sure we have a proper Filter element and get the partial SQL
299
if isinstance(filter, calendarqueryfilter.Filter):
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",
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()
317
maxDate = maxDate.duplicate()
318
maxDate.setDateOnly(True)
320
maxDate += PyCalendarDuration(days=365)
321
self.testAndUpdateIndex(maxDate)
323
# We cannot handle this filter in an indexed search
324
raise IndexedSearchException()
330
if qualifiers is None:
331
rowiter = self._db_execute("select NAME, UID, TYPE from RESOURCE")
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" +
341
rowiter = self._db_execute("select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE" + qualifiers[0], *qualifiers[1])
343
# Check result for missing resources
347
if self.resource.getChild(name.encode("utf-8")):
355
log.err("Calendar resource %s is missing from %s. Removing from index."
356
% (name, self.resource))
357
self.deleteResource(name)
361
def bruteForceSearch(self):
363
List the whole index and tests for existence, updating the index
364
@return: all resources in the index
367
rowiter = self._db_execute("select NAME, UID, TYPE from RESOURCE")
369
# Check result for missing resources:
374
if self.resource.getChild(name.encode("utf-8")):
377
log.err("Calendar resource %s is missing from %s. Removing from index."
378
% (name, self.resource))
379
self.deleteResource(name)
384
def _db_version(self):
386
@return: the schema version assigned to this index.
388
return schema_version
390
def _add_to_db(self, name, calendar, cursor=None, expand_until=None, reCreate=False):
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
401
raise NotImplementedError
403
def _delete_from_db(self, name, uid, dorevision=True):
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.
409
raise NotImplementedError
411
class CalendarIndex (AbstractCalendarIndex):
413
Calendar index - abstract class for indexer that indexes calendar objects in a collection.
416
def __init__(self, resource):
418
@param resource: the L{CalDAVResource} resource to
421
super(CalendarIndex, self).__init__(resource)
423
def _db_init_data_tables_base(self, q, uidunique):
425
Initialise the underlying database tables.
426
@param q: a database cursor to use.
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
438
create table RESOURCE (
439
RESOURCEID integer primary key autoincrement,
446
""" % (" unique" if uidunique else "",)
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
455
# FBTYPE: FBTYPE value:
459
# 'U' - busy-unavailable
460
# 'T' - busy-tentative
461
# TRANSPARENT: Y if transparent, N if opaque (default non-per-user value)
465
create table TIMESPAN (
466
INSTANCEID integer primary key autoincrement,
478
create index STARTENDFLOAT on TIMESPAN (START, END, FLOAT)
483
# PERUSER table tracks per-user ids
484
# PERUSERID: autoincrement primary key
485
# UID: User ID used in calendar data
489
create table PERUSER (
490
PERUSERID integer primary key autoincrement,
497
create index PERUSER_UID on PERUSER (USERUID)
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
509
create table TRANSPARENCY (
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
525
create table REVISION_SEQUENCE (
532
insert into REVISION_SEQUENCE (REVISION) values (0)
537
create table REVISIONS (
546
create index REVISION on REVISIONS (REVISION)
552
# RESERVED table tracks reserved UIDs
553
# UID: The UID being reserved
554
# TIME: When the reservation was made
558
create table RESERVED (
565
# Cascading triggers to help on delete
568
create trigger resourceDelete after delete on RESOURCE
571
delete from TIMESPAN where TIMESPAN.RESOURCEID = OLD.RESOURCEID;
577
create trigger timespanDelete after delete on TIMESPAN
580
delete from TRANSPARENCY where INSTANCEID = OLD.INSTANCEID;
585
def _db_can_upgrade(self, old_version):
587
Can we do an in-place upgrade
590
# v10 is a big change - no upgrade possible
593
def _db_upgrade_data_tables(self, q, old_version):
595
Upgrade the data from an older version of the DB.
598
# v10 is a big change - no upgrade possible
601
def notExpandedBeyond(self, minDate):
603
Gives all resources which have not been expanded beyond a given date
606
return self._db_values_for_sql("select NAME from RESOURCE where RECURRANCE_MAX < :1", pyCalendarTodatetime(minDate))
608
def reExpandResource(self, name, expand_until):
610
Given a resource name, remove it from the database and re-add it
611
with a longer expansion.
613
calendar = self.resource.getChild(name).iCalendar()
614
self._add_to_db(name, calendar, expand_until=expand_until, reCreate=True)
617
def _add_to_db(self, name, calendar, cursor = None, expand_until=None, reCreate=False):
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
628
uid = calendar.resourceUID()
629
organizer = calendar.getOrganizer()
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
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
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))
653
if expand_until and expand_until > expand:
654
expand = expand_until
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.
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()
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.
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,))
681
# Now coerce indexing to off if needed
682
if not doInstanceIndexing:
684
recurrenceLimit = PyCalendarDateTime(1900, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
686
self._delete_from_db(name, uid, False)
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
695
resourceid = self.lastrowid
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()
702
for useruid in useruids:
703
peruserid = self._db_value_for_sql(
704
"select PERUSERID from PERUSER where USERUID = :1",
707
if peruserid is None:
710
insert into PERUSER (USERUID)
714
peruserid = self.lastrowid
715
useruidmap[useruid] = peruserid
717
if doInstanceIndexing:
718
for key in instances:
719
instance = instances[key]
720
start = instance.start
722
float = 'Y' if instance.start.floating() else 'N'
723
transp = 'T' if instance.component.propertyValue("TRANSP") == "TRANSPARENT" else 'F'
726
insert into TIMESPAN (RESOURCEID, FLOAT, START, END, FBTYPE, TRANSPARENT)
727
values (:1, :2, :3, :4, :5, :6)
731
pyCalendarTodatetime(start),
732
pyCalendarTodatetime(end),
733
icalfbtype_to_indexfbtype.get(instance.component.getFBType(), 'F'),
736
instanceid = self.lastrowid
737
peruserdata = calendar.perUserTransparency(instance.rid)
738
for useruid, transp in peruserdata:
739
peruserid = useruidmap[useruid]
742
insert into TRANSPARENCY (PERUSERID, INSTANCEID, TRANSPARENT)
744
""", peruserid, instanceid, 'T' if transp else 'F'
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))
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), '?', '?'
760
instanceid = self.lastrowid
761
peruserdata = calendar.perUserTransparency(None)
762
for useruid, transp in peruserdata:
763
peruserid = useruidmap[useruid]
766
insert into TRANSPARENCY (PERUSERID, INSTANCEID, TRANSPARENT)
768
""", peruserid, instanceid, 'T' if transp else 'F'
773
insert or replace into REVISIONS (NAME, REVISION, DELETED)
775
""", name, self.bumpRevision(fast=True), 'N',
778
def _delete_from_db(self, name, uid, dorevision=True):
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.
784
self._db_execute("delete from RESOURCE where NAME = :1", name)
788
update REVISIONS SET REVISION = :1, DELETED = :2
790
""", self.bumpRevision(fast=True), 'Y', name
794
def wrapInDeferred(f):
795
def _(*args, **kwargs):
796
return maybeDeferred(f, *args, **kwargs)
801
class MemcachedUIDReserver(CachePoolUserMixIn, LoggingMixIn):
802
def __init__(self, index, cachePool=None):
804
self._cachePool = cachePool
807
return 'reservation:%s' % (
808
hashlib.md5('%s:%s' % (uid,
809
self.index.resource.fp.path)).hexdigest())
811
def reserveUID(self, uid):
812
uid = uid.encode('utf-8')
813
self.log_debug("Reserving UID %r @ %r" % (
815
self.index.resource.fp.path))
817
def _handleFalse(result):
819
raise ReservationError(
820
"UID %s already reserved for calendar collection %s."
821
% (uid, self.index.resource)
824
d = self.getCachePool().add(self._key(uid),
826
expireTime=config.UIDReservationTimeOut)
827
d.addCallback(_handleFalse)
831
def unreserveUID(self, uid):
832
uid = uid.encode('utf-8')
833
self.log_debug("Unreserving UID %r @ %r" % (
835
self.index.resource.fp.path))
837
def _handleFalse(result):
839
raise ReservationError(
840
"UID %s is not reserved for calendar collection %s."
841
% (uid, self.index.resource)
844
d =self.getCachePool().delete(self._key(uid))
845
d.addCallback(_handleFalse)
849
def isReservedUID(self, uid):
850
uid = uid.encode('utf-8')
851
self.log_debug("Is reserved UID %r @ %r" % (
853
self.index.resource.fp.path))
855
def _checkValue((flags, value)):
861
d = self.getCachePool().get(self._key(uid))
862
d.addCallback(_checkValue)
867
class SQLUIDReserver(object):
868
def __init__(self, index):
872
def reserveUID(self, uid):
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
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)
888
except sqlite.Error, e:
889
log.err("Unable to reserve UID: %s", (e,))
890
self.index._db_rollback()
893
def unreserveUID(self, uid):
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
902
raise ReservationError(
903
"UID %s is not reserved for calendar collection %s."
904
% (uid, self.index.resource)
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()
916
d = self.isReservedUID(uid)
922
def isReservedUID(self, uid):
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.
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):
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()
951
class Index (CalendarIndex):
953
Calendar collection index - regular collection that enforces CalDAV UID uniqueness requirement.
956
def __init__(self, resource):
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}.)
962
assert resource.isCalendarCollection(), "non-calendar collection resource %s has no index." % (resource,)
963
super(Index, self).__init__(resource)
966
hasattr(config, "Memcached") and
967
config.Memcached.Pools.Default.ClientEnabled
969
self.reserver = MemcachedUIDReserver(self)
971
self.reserver = SQLUIDReserver(self)
974
# A dict of sets. The dict keys are calendar collection paths,
975
# and the sets contains reserved UIDs for each path.
978
def reserveUID(self, uid):
979
return self.reserver.reserveUID(uid)
982
def unreserveUID(self, uid):
983
return self.reserver.unreserveUID(uid)
986
def isReservedUID(self, uid):
987
return self.reserver.isReservedUID(uid)
990
def isAllowedUID(self, uid, *names):
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,
1001
rname = self.resourceNameForUID(uid)
1002
return (rname is None or rname in names)
1006
@return: the collection type assigned to this index.
1008
return collection_types["Calendar"]
1010
def _db_init_data_tables(self, q):
1012
Initialise the underlying database tables.
1013
@param q: a database cursor to use.
1016
# Create database where the RESOURCE table has unique UID column.
1017
self._db_init_data_tables_base(q, True)
1019
def _db_recreate(self, do_commit=True):
1021
Re-create the database tables from existing calendar data.
1025
# Populate the DB with data from already existing resources.
1026
# This allows for index recovery if the DB file gets
1029
fp = self.resource.fp
1030
for name in fp.listdir():
1031
if name.startswith("."):
1035
stream = fp.child(name).open()
1036
except (IOError, OSError), e:
1037
log.err("Unable to open resource %s: %s" % (name, e))
1040
# FIXME: This is blocking I/O
1042
calendar = Component.fromStream(stream)
1043
calendar.validCalendarData()
1044
calendar.validCalendarForCalDAV(methodAllowed=False)
1046
log.err("Non-calendar resource: %s" % (name,))
1048
#log.msg("Indexing resource: %s" % (name,))
1049
self.addResource(name, calendar, True, reCreate=True)
1053
# Do commit outside of the loop for better performance
1057
class IndexSchedule (CalendarIndex):
1059
Schedule collection index - does not require UID uniqueness.
1062
def reserveUID(self, uid): #@UnusedVariable
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
1069
# iTIP does not require unique UIDs
1070
return succeed(None)
1072
def unreserveUID(self, uid): #@UnusedVariable
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
1079
# iTIP does not require unique UIDs
1080
return succeed(None)
1082
def isReservedUID(self, uid): #@UnusedVariable
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.
1089
# iTIP does not require unique UIDs
1090
return succeed(False)
1092
def isAllowedUID(self, uid, *names): #@UnusedVariable
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
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,
1105
# iTIP does not require unique UIDs
1110
@return: the collection type assigned to this index.
1112
return collection_types["iTIP"]
1114
def _db_init_data_tables(self, q):
1116
Initialise the underlying database tables.
1117
@param q: a database cursor to use.
1120
# Create database where the RESOURCE table has a UID column that is not unique.
1121
self._db_init_data_tables_base(q, False)
1123
def _db_recreate(self, do_commit=True):
1125
Re-create the database tables from existing calendar data.
1129
# Populate the DB with data from already existing resources.
1130
# This allows for index recovery if the DB file gets
1133
fp = self.resource.fp
1134
for name in fp.listdir():
1135
if name.startswith("."):
1139
stream = fp.child(name).open()
1140
except (IOError, OSError), e:
1141
log.err("Unable to open resource %s: %s" % (name, e))
1144
# FIXME: This is blocking I/O
1146
calendar = Component.fromStream(stream)
1147
calendar.validCalendarData()
1148
calendar.validCalendarForCalDAV(methodAllowed=True)
1150
log.err("Non-calendar resource: %s" % (name,))
1152
#log.msg("Indexing resource: %s" % (name,))
1153
self.addResource(name, calendar, True, reCreate=True)
1157
# Do commit outside of the loop for better performance