1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imiptools/stores/file.py Sun Mar 06 00:46:26 2016 +0100
1.3 @@ -0,0 +1,956 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +A simple filesystem-based store of calendar data.
1.8 +
1.9 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
1.10 +
1.11 +This program is free software; you can redistribute it and/or modify it under
1.12 +the terms of the GNU General Public License as published by the Free Software
1.13 +Foundation; either version 3 of the License, or (at your option) any later
1.14 +version.
1.15 +
1.16 +This program is distributed in the hope that it will be useful, but WITHOUT
1.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1.19 +details.
1.20 +
1.21 +You should have received a copy of the GNU General Public License along with
1.22 +this program. If not, see <http://www.gnu.org/licenses/>.
1.23 +"""
1.24 +
1.25 +from imiptools.stores import StoreBase, PublisherBase, JournalBase
1.26 +
1.27 +from datetime import datetime
1.28 +from imiptools.config import STORE_DIR, PUBLISH_DIR, JOURNAL_DIR
1.29 +from imiptools.data import make_calendar, parse_object, to_stream
1.30 +from imiptools.dates import format_datetime, get_datetime, to_timezone
1.31 +from imiptools.filesys import fix_permissions, FileBase
1.32 +from imiptools.period import FreeBusyPeriod, FreeBusyCollection
1.33 +from imiptools.text import parse_line
1.34 +from os.path import isdir, isfile, join
1.35 +from os import listdir, remove, rmdir
1.36 +import codecs
1.37 +
1.38 +class FileStoreBase(FileBase):
1.39 +
1.40 + "A file store supporting user-specific locking and tabular data."
1.41 +
1.42 + def acquire_lock(self, user, timeout=None):
1.43 + FileBase.acquire_lock(self, timeout, user)
1.44 +
1.45 + def release_lock(self, user):
1.46 + FileBase.release_lock(self, user)
1.47 +
1.48 + # Utility methods.
1.49 +
1.50 + def _set_defaults(self, t, empty_defaults):
1.51 + for i, default in empty_defaults:
1.52 + if i >= len(t):
1.53 + t += [None] * (i - len(t) + 1)
1.54 + if not t[i]:
1.55 + t[i] = default
1.56 + return t
1.57 +
1.58 + def _get_table(self, user, filename, empty_defaults=None, tab_separated=True):
1.59 +
1.60 + """
1.61 + From the file for the given 'user' having the given 'filename', return
1.62 + a list of tuples representing the file's contents.
1.63 +
1.64 + The 'empty_defaults' is a list of (index, value) tuples indicating the
1.65 + default value where a column either does not exist or provides an empty
1.66 + value.
1.67 +
1.68 + If 'tab_separated' is specified and is a false value, line parsing using
1.69 + the imiptools.text.parse_line function will be performed instead of
1.70 + splitting each line of the file using tab characters as separators.
1.71 + """
1.72 +
1.73 + f = codecs.open(filename, "rb", encoding="utf-8")
1.74 + try:
1.75 + l = []
1.76 + for line in f.readlines():
1.77 + line = line.strip(" \r\n")
1.78 + if tab_separated:
1.79 + t = line.split("\t")
1.80 + else:
1.81 + t = parse_line(line)
1.82 + if empty_defaults:
1.83 + t = self._set_defaults(t, empty_defaults)
1.84 + l.append(tuple(t))
1.85 + return l
1.86 + finally:
1.87 + f.close()
1.88 +
1.89 + def _get_table_atomic(self, user, filename, empty_defaults=None, tab_separated=True):
1.90 +
1.91 + """
1.92 + From the file for the given 'user' having the given 'filename', return
1.93 + a list of tuples representing the file's contents.
1.94 +
1.95 + The 'empty_defaults' is a list of (index, value) tuples indicating the
1.96 + default value where a column either does not exist or provides an empty
1.97 + value.
1.98 +
1.99 + If 'tab_separated' is specified and is a false value, line parsing using
1.100 + the imiptools.text.parse_line function will be performed instead of
1.101 + splitting each line of the file using tab characters as separators.
1.102 + """
1.103 +
1.104 + self.acquire_lock(user)
1.105 + try:
1.106 + return self._get_table(user, filename, empty_defaults, tab_separated)
1.107 + finally:
1.108 + self.release_lock(user)
1.109 +
1.110 + def _set_table(self, user, filename, items, empty_defaults=None):
1.111 +
1.112 + """
1.113 + For the given 'user', write to the file having the given 'filename' the
1.114 + 'items'.
1.115 +
1.116 + The 'empty_defaults' is a list of (index, value) tuples indicating the
1.117 + default value where a column either does not exist or provides an empty
1.118 + value.
1.119 + """
1.120 +
1.121 + f = codecs.open(filename, "wb", encoding="utf-8")
1.122 + try:
1.123 + for item in items:
1.124 + self._set_table_item(f, item, empty_defaults)
1.125 + finally:
1.126 + f.close()
1.127 + fix_permissions(filename)
1.128 +
1.129 + def _set_table_item(self, f, item, empty_defaults=None):
1.130 +
1.131 + "Set in table 'f' the given 'item', using any 'empty_defaults'."
1.132 +
1.133 + if empty_defaults:
1.134 + item = self._set_defaults(list(item), empty_defaults)
1.135 + f.write("\t".join(item) + "\n")
1.136 +
1.137 + def _set_table_atomic(self, user, filename, items, empty_defaults=None):
1.138 +
1.139 + """
1.140 + For the given 'user', write to the file having the given 'filename' the
1.141 + 'items'.
1.142 +
1.143 + The 'empty_defaults' is a list of (index, value) tuples indicating the
1.144 + default value where a column either does not exist or provides an empty
1.145 + value.
1.146 + """
1.147 +
1.148 + self.acquire_lock(user)
1.149 + try:
1.150 + self._set_table(user, filename, items, empty_defaults)
1.151 + finally:
1.152 + self.release_lock(user)
1.153 +
1.154 +class FileStore(FileStoreBase, StoreBase):
1.155 +
1.156 + "A file store of tabular free/busy data and objects."
1.157 +
1.158 + def __init__(self, store_dir=None):
1.159 + FileBase.__init__(self, store_dir or STORE_DIR)
1.160 +
1.161 + # Store object access.
1.162 +
1.163 + def _get_object(self, user, filename):
1.164 +
1.165 + """
1.166 + Return the parsed object for the given 'user' having the given
1.167 + 'filename'.
1.168 + """
1.169 +
1.170 + self.acquire_lock(user)
1.171 + try:
1.172 + f = open(filename, "rb")
1.173 + try:
1.174 + return parse_object(f, "utf-8")
1.175 + finally:
1.176 + f.close()
1.177 + finally:
1.178 + self.release_lock(user)
1.179 +
1.180 + def _set_object(self, user, filename, node):
1.181 +
1.182 + """
1.183 + Set an object for the given 'user' having the given 'filename', using
1.184 + 'node' to define the object.
1.185 + """
1.186 +
1.187 + self.acquire_lock(user)
1.188 + try:
1.189 + f = open(filename, "wb")
1.190 + try:
1.191 + to_stream(f, node)
1.192 + finally:
1.193 + f.close()
1.194 + fix_permissions(filename)
1.195 + finally:
1.196 + self.release_lock(user)
1.197 +
1.198 + return True
1.199 +
1.200 + def _remove_object(self, filename):
1.201 +
1.202 + "Remove the object with the given 'filename'."
1.203 +
1.204 + try:
1.205 + remove(filename)
1.206 + except OSError:
1.207 + return False
1.208 +
1.209 + return True
1.210 +
1.211 + def _remove_collection(self, filename):
1.212 +
1.213 + "Remove the collection with the given 'filename'."
1.214 +
1.215 + try:
1.216 + rmdir(filename)
1.217 + except OSError:
1.218 + return False
1.219 +
1.220 + return True
1.221 +
1.222 + # User discovery.
1.223 +
1.224 + def get_users(self):
1.225 +
1.226 + "Return a list of users."
1.227 +
1.228 + return listdir(self.store_dir)
1.229 +
1.230 + # Event and event metadata access.
1.231 +
1.232 + def get_events(self, user):
1.233 +
1.234 + "Return a list of event identifiers."
1.235 +
1.236 + filename = self.get_object_in_store(user, "objects")
1.237 + if not filename or not isdir(filename):
1.238 + return None
1.239 +
1.240 + return [name for name in listdir(filename) if isfile(join(filename, name))]
1.241 +
1.242 + def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
1.243 +
1.244 + """
1.245 + Get the filename providing the event for the given 'user' with the given
1.246 + 'uid'. If the optional 'recurrenceid' is specified, a specific instance
1.247 + or occurrence of an event is returned.
1.248 +
1.249 + Where 'dirname' is specified, the given directory name is used as the
1.250 + base of the location within which any filename will reside.
1.251 + """
1.252 +
1.253 + if recurrenceid:
1.254 + return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
1.255 + else:
1.256 + return self.get_complete_event_filename(user, uid, dirname, username)
1.257 +
1.258 + def get_event(self, user, uid, recurrenceid=None, dirname=None):
1.259 +
1.260 + """
1.261 + Get the event for the given 'user' with the given 'uid'. If
1.262 + the optional 'recurrenceid' is specified, a specific instance or
1.263 + occurrence of an event is returned.
1.264 + """
1.265 +
1.266 + filename = self.get_event_filename(user, uid, recurrenceid, dirname)
1.267 + if not filename or not isfile(filename):
1.268 + return None
1.269 +
1.270 + return filename and self._get_object(user, filename)
1.271 +
1.272 + def get_complete_event_filename(self, user, uid, dirname=None, username=None):
1.273 +
1.274 + """
1.275 + Get the filename providing the event for the given 'user' with the given
1.276 + 'uid'.
1.277 +
1.278 + Where 'dirname' is specified, the given directory name is used as the
1.279 + base of the location within which any filename will reside.
1.280 +
1.281 + Where 'username' is specified, the event details will reside in a file
1.282 + bearing that name within a directory having 'uid' as its name.
1.283 + """
1.284 +
1.285 + return self.get_object_in_store(user, dirname, "objects", uid, username)
1.286 +
1.287 + def get_complete_event(self, user, uid):
1.288 +
1.289 + "Get the event for the given 'user' with the given 'uid'."
1.290 +
1.291 + filename = self.get_complete_event_filename(user, uid)
1.292 + if not filename or not isfile(filename):
1.293 + return None
1.294 +
1.295 + return filename and self._get_object(user, filename)
1.296 +
1.297 + def set_complete_event(self, user, uid, node):
1.298 +
1.299 + "Set an event for 'user' having the given 'uid' and 'node'."
1.300 +
1.301 + filename = self.get_object_in_store(user, "objects", uid)
1.302 + if not filename:
1.303 + return False
1.304 +
1.305 + return self._set_object(user, filename, node)
1.306 +
1.307 + def remove_parent_event(self, user, uid):
1.308 +
1.309 + "Remove the parent event for 'user' having the given 'uid'."
1.310 +
1.311 + filename = self.get_object_in_store(user, "objects", uid)
1.312 + if not filename:
1.313 + return False
1.314 +
1.315 + return self._remove_object(filename)
1.316 +
1.317 + def get_recurrences(self, user, uid):
1.318 +
1.319 + """
1.320 + Get additional event instances for an event of the given 'user' with the
1.321 + indicated 'uid'. Both active and cancelled recurrences are returned.
1.322 + """
1.323 +
1.324 + return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
1.325 +
1.326 + def get_active_recurrences(self, user, uid):
1.327 +
1.328 + """
1.329 + Get additional event instances for an event of the given 'user' with the
1.330 + indicated 'uid'. Cancelled recurrences are not returned.
1.331 + """
1.332 +
1.333 + filename = self.get_object_in_store(user, "recurrences", uid)
1.334 + if not filename or not isdir(filename):
1.335 + return []
1.336 +
1.337 + return [name for name in listdir(filename) if isfile(join(filename, name))]
1.338 +
1.339 + def get_cancelled_recurrences(self, user, uid):
1.340 +
1.341 + """
1.342 + Get additional event instances for an event of the given 'user' with the
1.343 + indicated 'uid'. Only cancelled recurrences are returned.
1.344 + """
1.345 +
1.346 + filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
1.347 + if not filename or not isdir(filename):
1.348 + return []
1.349 +
1.350 + return [name for name in listdir(filename) if isfile(join(filename, name))]
1.351 +
1.352 + def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
1.353 +
1.354 + """
1.355 + For the event of the given 'user' with the given 'uid', return the
1.356 + filename providing the recurrence with the given 'recurrenceid'.
1.357 +
1.358 + Where 'dirname' is specified, the given directory name is used as the
1.359 + base of the location within which any filename will reside.
1.360 +
1.361 + Where 'username' is specified, the event details will reside in a file
1.362 + bearing that name within a directory having 'uid' as its name.
1.363 + """
1.364 +
1.365 + return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
1.366 +
1.367 + def get_recurrence(self, user, uid, recurrenceid):
1.368 +
1.369 + """
1.370 + For the event of the given 'user' with the given 'uid', return the
1.371 + specific recurrence indicated by the 'recurrenceid'.
1.372 + """
1.373 +
1.374 + filename = self.get_recurrence_filename(user, uid, recurrenceid)
1.375 + if not filename or not isfile(filename):
1.376 + return None
1.377 +
1.378 + return filename and self._get_object(user, filename)
1.379 +
1.380 + def set_recurrence(self, user, uid, recurrenceid, node):
1.381 +
1.382 + "Set an event for 'user' having the given 'uid' and 'node'."
1.383 +
1.384 + filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
1.385 + if not filename:
1.386 + return False
1.387 +
1.388 + return self._set_object(user, filename, node)
1.389 +
1.390 + def remove_recurrence(self, user, uid, recurrenceid):
1.391 +
1.392 + """
1.393 + Remove a special recurrence from an event stored by 'user' having the
1.394 + given 'uid' and 'recurrenceid'.
1.395 + """
1.396 +
1.397 + filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
1.398 + if not filename:
1.399 + return False
1.400 +
1.401 + return self._remove_object(filename)
1.402 +
1.403 + def remove_recurrence_collection(self, user, uid):
1.404 +
1.405 + """
1.406 + Remove the collection of recurrences stored by 'user' having the given
1.407 + 'uid'.
1.408 + """
1.409 +
1.410 + recurrences = self.get_object_in_store(user, "recurrences", uid)
1.411 + if recurrences:
1.412 + return self._remove_collection(recurrences)
1.413 +
1.414 + return True
1.415 +
1.416 + # Free/busy period providers, upon extension of the free/busy records.
1.417 +
1.418 + def _get_freebusy_providers(self, user):
1.419 +
1.420 + """
1.421 + Return the free/busy providers for the given 'user'.
1.422 +
1.423 + This function returns any stored datetime and a list of providers as a
1.424 + 2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
1.425 + """
1.426 +
1.427 + filename = self.get_object_in_store(user, "freebusy-providers")
1.428 + if not filename or not isfile(filename):
1.429 + return None
1.430 +
1.431 + # Attempt to read providers, with a declaration of the datetime
1.432 + # from which such providers are considered as still being active.
1.433 +
1.434 + t = self._get_table_atomic(user, filename, [(1, None)])
1.435 + try:
1.436 + dt_string = t[0][0]
1.437 + except IndexError:
1.438 + return None
1.439 +
1.440 + return dt_string, t[1:]
1.441 +
1.442 + def get_freebusy_providers(self, user, dt=None):
1.443 +
1.444 + """
1.445 + Return a set of uncancelled events of the form (uid, recurrenceid)
1.446 + providing free/busy details beyond the given datetime 'dt'.
1.447 +
1.448 + If 'dt' is not specified, all events previously found to provide
1.449 + details will be returned. Otherwise, if 'dt' is earlier than the
1.450 + datetime recorded for the known providers, None is returned, indicating
1.451 + that the list of providers must be recomputed.
1.452 +
1.453 + This function returns a list of (uid, recurrenceid) tuples upon success.
1.454 + """
1.455 +
1.456 + t = self._get_freebusy_providers(user)
1.457 + if not t:
1.458 + return None
1.459 +
1.460 + dt_string, t = t
1.461 +
1.462 + # If the requested datetime is earlier than the stated datetime, the
1.463 + # providers will need to be recomputed.
1.464 +
1.465 + if dt:
1.466 + providers_dt = get_datetime(dt_string)
1.467 + if not providers_dt or providers_dt > dt:
1.468 + return None
1.469 +
1.470 + # Otherwise, return the providers.
1.471 +
1.472 + return t[1:]
1.473 +
1.474 + def _set_freebusy_providers(self, user, dt_string, t):
1.475 +
1.476 + "Set the given provider timestamp 'dt_string' and table 't'."
1.477 +
1.478 + filename = self.get_object_in_store(user, "freebusy-providers")
1.479 + if not filename:
1.480 + return False
1.481 +
1.482 + t.insert(0, (dt_string,))
1.483 + self._set_table_atomic(user, filename, t, [(1, "")])
1.484 + return True
1.485 +
1.486 + def set_freebusy_providers(self, user, dt, providers):
1.487 +
1.488 + """
1.489 + Define the uncancelled events providing free/busy details beyond the
1.490 + given datetime 'dt'.
1.491 + """
1.492 +
1.493 + t = []
1.494 +
1.495 + for obj in providers:
1.496 + t.append((obj.get_uid(), obj.get_recurrenceid()))
1.497 +
1.498 + return self._set_freebusy_providers(user, format_datetime(dt), t)
1.499 +
1.500 + def append_freebusy_provider(self, user, provider):
1.501 +
1.502 + "For the given 'user', append the free/busy 'provider'."
1.503 +
1.504 + t = self._get_freebusy_providers(user)
1.505 + if not t:
1.506 + return False
1.507 +
1.508 + dt_string, t = t
1.509 + t.append((provider.get_uid(), provider.get_recurrenceid()))
1.510 +
1.511 + return self._set_freebusy_providers(user, dt_string, t)
1.512 +
1.513 + def remove_freebusy_provider(self, user, provider):
1.514 +
1.515 + "For the given 'user', remove the free/busy 'provider'."
1.516 +
1.517 + t = self._get_freebusy_providers(user)
1.518 + if not t:
1.519 + return False
1.520 +
1.521 + dt_string, t = t
1.522 + try:
1.523 + t.remove((provider.get_uid(), provider.get_recurrenceid()))
1.524 + except ValueError:
1.525 + return False
1.526 +
1.527 + return self._set_freebusy_providers(user, dt_string, t)
1.528 +
1.529 + # Free/busy period access.
1.530 +
1.531 + def get_freebusy(self, user, name=None):
1.532 +
1.533 + "Get free/busy details for the given 'user'."
1.534 +
1.535 + filename = self.get_object_in_store(user, name or "freebusy")
1.536 +
1.537 + if not filename or not isfile(filename):
1.538 + periods = []
1.539 + else:
1.540 + periods = map(lambda t: FreeBusyPeriod(*t),
1.541 + self._get_table_atomic(user, filename))
1.542 +
1.543 + return FreeBusyCollection(periods)
1.544 +
1.545 + def get_freebusy_for_other(self, user, other):
1.546 +
1.547 + "For the given 'user', get free/busy details for the 'other' user."
1.548 +
1.549 + filename = self.get_object_in_store(user, "freebusy-other", other)
1.550 +
1.551 + if not filename or not isfile(filename):
1.552 + periods = []
1.553 + else:
1.554 + periods = map(lambda t: FreeBusyPeriod(*t),
1.555 + self._get_table_atomic(user, filename))
1.556 +
1.557 + return FreeBusyCollection(periods)
1.558 +
1.559 + def set_freebusy(self, user, freebusy, name=None):
1.560 +
1.561 + "For the given 'user', set 'freebusy' details."
1.562 +
1.563 + filename = self.get_object_in_store(user, name or "freebusy")
1.564 + if not filename:
1.565 + return False
1.566 +
1.567 + self._set_table_atomic(user, filename,
1.568 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.569 + return True
1.570 +
1.571 + def set_freebusy_for_other(self, user, freebusy, other):
1.572 +
1.573 + "For the given 'user', set 'freebusy' details for the 'other' user."
1.574 +
1.575 + filename = self.get_object_in_store(user, "freebusy-other", other)
1.576 + if not filename:
1.577 + return False
1.578 +
1.579 + self._set_table_atomic(user, filename,
1.580 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.581 + return True
1.582 +
1.583 + # Tentative free/busy periods related to countering.
1.584 +
1.585 + def get_freebusy_offers(self, user):
1.586 +
1.587 + "Get free/busy offers for the given 'user'."
1.588 +
1.589 + offers = []
1.590 + expired = []
1.591 + now = to_timezone(datetime.utcnow(), "UTC")
1.592 +
1.593 + # Expire old offers and save the collection if modified.
1.594 +
1.595 + self.acquire_lock(user)
1.596 + try:
1.597 + l = self.get_freebusy(user, "freebusy-offers")
1.598 + for fb in l:
1.599 + if fb.expires and get_datetime(fb.expires) <= now:
1.600 + expired.append(fb)
1.601 + else:
1.602 + offers.append(fb)
1.603 +
1.604 + if expired:
1.605 + self.set_freebusy_offers(user, offers)
1.606 + finally:
1.607 + self.release_lock(user)
1.608 +
1.609 + return FreeBusyCollection(offers)
1.610 +
1.611 + def set_freebusy_offers(self, user, freebusy):
1.612 +
1.613 + "For the given 'user', set 'freebusy' offers."
1.614 +
1.615 + return self.set_freebusy(user, freebusy, "freebusy-offers")
1.616 +
1.617 + # Requests and counter-proposals.
1.618 +
1.619 + def _get_requests(self, user, queue):
1.620 +
1.621 + "Get requests for the given 'user' from the given 'queue'."
1.622 +
1.623 + filename = self.get_object_in_store(user, queue)
1.624 + if not filename or not isfile(filename):
1.625 + return None
1.626 +
1.627 + return self._get_table_atomic(user, filename, [(1, None), (2, None)])
1.628 +
1.629 + def get_requests(self, user):
1.630 +
1.631 + "Get requests for the given 'user'."
1.632 +
1.633 + return self._get_requests(user, "requests")
1.634 +
1.635 + def _set_requests(self, user, requests, queue):
1.636 +
1.637 + """
1.638 + For the given 'user', set the list of queued 'requests' in the given
1.639 + 'queue'.
1.640 + """
1.641 +
1.642 + filename = self.get_object_in_store(user, queue)
1.643 + if not filename:
1.644 + return False
1.645 +
1.646 + self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])
1.647 + return True
1.648 +
1.649 + def set_requests(self, user, requests):
1.650 +
1.651 + "For the given 'user', set the list of queued 'requests'."
1.652 +
1.653 + return self._set_requests(user, requests, "requests")
1.654 +
1.655 + def _set_request(self, user, request, queue):
1.656 +
1.657 + """
1.658 + For the given 'user', set the given 'request' in the given 'queue'.
1.659 + """
1.660 +
1.661 + filename = self.get_object_in_store(user, queue)
1.662 + if not filename:
1.663 + return False
1.664 +
1.665 + self.acquire_lock(user)
1.666 + try:
1.667 + f = codecs.open(filename, "ab", encoding="utf-8")
1.668 + try:
1.669 + self._set_table_item(f, request, [(1, ""), (2, "")])
1.670 + finally:
1.671 + f.close()
1.672 + fix_permissions(filename)
1.673 + finally:
1.674 + self.release_lock(user)
1.675 +
1.676 + return True
1.677 +
1.678 + def set_request(self, user, uid, recurrenceid=None, type=None):
1.679 +
1.680 + """
1.681 + For the given 'user', set the queued 'uid' and 'recurrenceid',
1.682 + indicating a request, along with any given 'type'.
1.683 + """
1.684 +
1.685 + return self._set_request(user, (uid, recurrenceid, type), "requests")
1.686 +
1.687 + def get_counters(self, user, uid, recurrenceid=None):
1.688 +
1.689 + """
1.690 + For the given 'user', return a list of users from whom counter-proposals
1.691 + have been received for the given 'uid' and optional 'recurrenceid'.
1.692 + """
1.693 +
1.694 + filename = self.get_event_filename(user, uid, recurrenceid, "counters")
1.695 + if not filename or not isdir(filename):
1.696 + return False
1.697 +
1.698 + return [name for name in listdir(filename) if isfile(join(filename, name))]
1.699 +
1.700 + def get_counter(self, user, other, uid, recurrenceid=None):
1.701 +
1.702 + """
1.703 + For the given 'user', return the counter-proposal from 'other' for the
1.704 + given 'uid' and optional 'recurrenceid'.
1.705 + """
1.706 +
1.707 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
1.708 + if not filename:
1.709 + return False
1.710 +
1.711 + return self._get_object(user, filename)
1.712 +
1.713 + def set_counter(self, user, other, node, uid, recurrenceid=None):
1.714 +
1.715 + """
1.716 + For the given 'user', store a counter-proposal received from 'other' the
1.717 + given 'node' representing that proposal for the given 'uid' and
1.718 + 'recurrenceid'.
1.719 + """
1.720 +
1.721 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
1.722 + if not filename:
1.723 + return False
1.724 +
1.725 + return self._set_object(user, filename, node)
1.726 +
1.727 + def remove_counters(self, user, uid, recurrenceid=None):
1.728 +
1.729 + """
1.730 + For the given 'user', remove all counter-proposals associated with the
1.731 + given 'uid' and 'recurrenceid'.
1.732 + """
1.733 +
1.734 + filename = self.get_event_filename(user, uid, recurrenceid, "counters")
1.735 + if not filename or not isdir(filename):
1.736 + return False
1.737 +
1.738 + removed = False
1.739 +
1.740 + for other in listdir(filename):
1.741 + counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
1.742 + removed = removed or self._remove_object(counter_filename)
1.743 +
1.744 + return removed
1.745 +
1.746 + def remove_counter(self, user, other, uid, recurrenceid=None):
1.747 +
1.748 + """
1.749 + For the given 'user', remove any counter-proposal from 'other'
1.750 + associated with the given 'uid' and 'recurrenceid'.
1.751 + """
1.752 +
1.753 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
1.754 + if not filename or not isfile(filename):
1.755 + return False
1.756 +
1.757 + return self._remove_object(filename)
1.758 +
1.759 + # Event cancellation.
1.760 +
1.761 + def cancel_event(self, user, uid, recurrenceid=None):
1.762 +
1.763 + """
1.764 + Cancel an event for 'user' having the given 'uid'. If the optional
1.765 + 'recurrenceid' is specified, a specific instance or occurrence of an
1.766 + event is cancelled.
1.767 + """
1.768 +
1.769 + filename = self.get_event_filename(user, uid, recurrenceid)
1.770 + cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
1.771 +
1.772 + if filename and cancelled_filename and isfile(filename):
1.773 + return self.move_object(filename, cancelled_filename)
1.774 +
1.775 + return False
1.776 +
1.777 + def uncancel_event(self, user, uid, recurrenceid=None):
1.778 +
1.779 + """
1.780 + Uncancel an event for 'user' having the given 'uid'. If the optional
1.781 + 'recurrenceid' is specified, a specific instance or occurrence of an
1.782 + event is uncancelled.
1.783 + """
1.784 +
1.785 + filename = self.get_event_filename(user, uid, recurrenceid)
1.786 + cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
1.787 +
1.788 + if filename and cancelled_filename and isfile(cancelled_filename):
1.789 + return self.move_object(cancelled_filename, filename)
1.790 +
1.791 + return False
1.792 +
1.793 + def remove_cancellation(self, user, uid, recurrenceid=None):
1.794 +
1.795 + """
1.796 + Remove a cancellation for 'user' for the event having the given 'uid'.
1.797 + If the optional 'recurrenceid' is specified, a specific instance or
1.798 + occurrence of an event is affected.
1.799 + """
1.800 +
1.801 + # Remove any parent event cancellation or a specific recurrence
1.802 + # cancellation if indicated.
1.803 +
1.804 + filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
1.805 +
1.806 + if filename and isfile(filename):
1.807 + return self._remove_object(filename)
1.808 +
1.809 + return False
1.810 +
1.811 +class FilePublisher(FileBase, PublisherBase):
1.812 +
1.813 + "A publisher of objects."
1.814 +
1.815 + def __init__(self, store_dir=None):
1.816 + FileBase.__init__(self, store_dir or PUBLISH_DIR)
1.817 +
1.818 + def set_freebusy(self, user, freebusy):
1.819 +
1.820 + "For the given 'user', set 'freebusy' details."
1.821 +
1.822 + filename = self.get_object_in_store(user, "freebusy")
1.823 + if not filename:
1.824 + return False
1.825 +
1.826 + record = []
1.827 + rwrite = record.append
1.828 +
1.829 + rwrite(("ORGANIZER", {}, user))
1.830 + rwrite(("UID", {}, user))
1.831 + rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
1.832 +
1.833 + for fb in freebusy:
1.834 + if not fb.transp or fb.transp == "OPAQUE":
1.835 + rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
1.836 + map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
1.837 +
1.838 + f = open(filename, "wb")
1.839 + try:
1.840 + to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
1.841 + finally:
1.842 + f.close()
1.843 + fix_permissions(filename)
1.844 +
1.845 + return True
1.846 +
1.847 +class FileJournal(FileStoreBase, JournalBase):
1.848 +
1.849 + "A journal system to support quotas."
1.850 +
1.851 + def __init__(self, store_dir=None):
1.852 + FileBase.__init__(self, store_dir or JOURNAL_DIR)
1.853 +
1.854 + # Quota and user identity/group discovery.
1.855 +
1.856 + def get_quotas(self):
1.857 +
1.858 + "Return a list of quotas."
1.859 +
1.860 + return listdir(self.store_dir)
1.861 +
1.862 + def get_quota_users(self, quota):
1.863 +
1.864 + "Return a list of quota users."
1.865 +
1.866 + filename = self.get_object_in_store(quota, "journal")
1.867 + if not filename or not isdir(filename):
1.868 + return []
1.869 +
1.870 + return listdir(filename)
1.871 +
1.872 + # Groups of users sharing quotas.
1.873 +
1.874 + def get_groups(self, quota):
1.875 +
1.876 + "Return the identity mappings for the given 'quota' as a dictionary."
1.877 +
1.878 + filename = self.get_object_in_store(quota, "groups")
1.879 + if not filename or not isfile(filename):
1.880 + return {}
1.881 +
1.882 + return dict(self._get_table_atomic(quota, filename, tab_separated=False))
1.883 +
1.884 + def get_limits(self, quota):
1.885 +
1.886 + """
1.887 + Return the limits for the 'quota' as a dictionary mapping identities or
1.888 + groups to durations.
1.889 + """
1.890 +
1.891 + filename = self.get_object_in_store(quota, "limits")
1.892 + if not filename or not isfile(filename):
1.893 + return None
1.894 +
1.895 + return dict(self._get_table_atomic(quota, filename, tab_separated=False))
1.896 +
1.897 + # Free/busy period access for users within quota groups.
1.898 +
1.899 + def get_freebusy(self, quota, user):
1.900 +
1.901 + "Get free/busy details for the given 'quota' and 'user'."
1.902 +
1.903 + filename = self.get_object_in_store(quota, "freebusy", user)
1.904 +
1.905 + if not filename or not isfile(filename):
1.906 + periods = []
1.907 + else:
1.908 + periods = map(lambda t: FreeBusyPeriod(*t),
1.909 + self._get_table_atomic(quota, filename))
1.910 +
1.911 + return FreeBusyCollection(periods)
1.912 +
1.913 + def set_freebusy(self, quota, user, freebusy):
1.914 +
1.915 + "For the given 'quota' and 'user', set 'freebusy' details."
1.916 +
1.917 + filename = self.get_object_in_store(quota, "freebusy", user)
1.918 + if not filename:
1.919 + return False
1.920 +
1.921 + self._set_table_atomic(quota, filename,
1.922 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.923 + return True
1.924 +
1.925 + # Journal entry methods.
1.926 +
1.927 + def get_entries(self, quota, group):
1.928 +
1.929 + """
1.930 + Return a list of journal entries for the given 'quota' for the indicated
1.931 + 'group'.
1.932 + """
1.933 +
1.934 + filename = self.get_object_in_store(quota, "journal", group)
1.935 +
1.936 + if not filename or not isfile(filename):
1.937 + periods = []
1.938 + else:
1.939 + periods = map(lambda t: FreeBusyPeriod(*t),
1.940 + self._get_table_atomic(quota, filename))
1.941 +
1.942 + return FreeBusyCollection(periods)
1.943 +
1.944 + def set_entries(self, quota, group, entries):
1.945 +
1.946 + """
1.947 + For the given 'quota' and indicated 'group', set the list of journal
1.948 + 'entries'.
1.949 + """
1.950 +
1.951 + filename = self.get_object_in_store(quota, "journal", group)
1.952 + if not filename:
1.953 + return False
1.954 +
1.955 + self._set_table_atomic(quota, filename,
1.956 + map(lambda fb: fb.as_tuple(strings_only=True), entries.periods))
1.957 + return True
1.958 +
1.959 +# vim: tabstop=4 expandtab shiftwidth=4