1 #!/usr/bin/env python 2 3 """ 4 A simple filesystem-based store of calendar data. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from datetime import datetime 23 from imiptools.config import STORE_DIR, PUBLISH_DIR 24 from imiptools.data import make_calendar, parse_object, to_stream 25 from imiptools.dates import format_datetime, get_datetime 26 from imiptools.filesys import fix_permissions, FileBase 27 from imiptools.period import FreeBusyPeriod 28 from os.path import exists, isfile, join 29 from os import listdir, remove, rmdir 30 from time import sleep 31 import codecs 32 33 class FileStore(FileBase): 34 35 "A file store of tabular free/busy data and objects." 36 37 def __init__(self, store_dir=None): 38 FileBase.__init__(self, store_dir or STORE_DIR) 39 40 def acquire_lock(self, user, timeout=None): 41 FileBase.acquire_lock(self, timeout, user) 42 43 def release_lock(self, user): 44 FileBase.release_lock(self, user) 45 46 # Utility methods. 47 48 def _set_defaults(self, t, empty_defaults): 49 for i, default in empty_defaults: 50 if i >= len(t): 51 t += [None] * (i - len(t) + 1) 52 if not t[i]: 53 t[i] = default 54 return t 55 56 def _get_table(self, user, filename, empty_defaults=None): 57 58 """ 59 From the file for the given 'user' having the given 'filename', return 60 a list of tuples representing the file's contents. 61 62 The 'empty_defaults' is a list of (index, value) tuples indicating the 63 default value where a column either does not exist or provides an empty 64 value. 65 """ 66 67 self.acquire_lock(user) 68 try: 69 f = codecs.open(filename, "rb", encoding="utf-8") 70 try: 71 l = [] 72 for line in f.readlines(): 73 t = line.strip(" \r\n").split("\t") 74 if empty_defaults: 75 t = self._set_defaults(t, empty_defaults) 76 l.append(tuple(t)) 77 return l 78 finally: 79 f.close() 80 finally: 81 self.release_lock(user) 82 83 def _set_table(self, user, filename, items, empty_defaults=None): 84 85 """ 86 For the given 'user', write to the file having the given 'filename' the 87 'items'. 88 89 The 'empty_defaults' is a list of (index, value) tuples indicating the 90 default value where a column either does not exist or provides an empty 91 value. 92 """ 93 94 self.acquire_lock(user) 95 try: 96 f = codecs.open(filename, "wb", encoding="utf-8") 97 try: 98 for item in items: 99 if empty_defaults: 100 item = self._set_defaults(list(item), empty_defaults) 101 f.write("\t".join(item) + "\n") 102 finally: 103 f.close() 104 fix_permissions(filename) 105 finally: 106 self.release_lock(user) 107 108 # Store object access. 109 110 def _get_object(self, user, filename): 111 112 """ 113 Return the parsed object for the given 'user' having the given 114 'filename'. 115 """ 116 117 self.acquire_lock(user) 118 try: 119 f = open(filename, "rb") 120 try: 121 return parse_object(f, "utf-8") 122 finally: 123 f.close() 124 finally: 125 self.release_lock(user) 126 127 def _set_object(self, user, filename, node): 128 129 """ 130 Set an object for the given 'user' having the given 'filename', using 131 'node' to define the object. 132 """ 133 134 self.acquire_lock(user) 135 try: 136 f = open(filename, "wb") 137 try: 138 to_stream(f, node) 139 finally: 140 f.close() 141 fix_permissions(filename) 142 finally: 143 self.release_lock(user) 144 145 return True 146 147 def _remove_object(self, filename): 148 149 "Remove the object with the given 'filename'." 150 151 try: 152 remove(filename) 153 except OSError: 154 return False 155 156 return True 157 158 def _remove_collection(self, filename): 159 160 "Remove the collection with the given 'filename'." 161 162 try: 163 rmdir(filename) 164 except OSError: 165 return False 166 167 return True 168 169 # Event and event metadata access. 170 171 def get_events(self, user): 172 173 "Return a list of event identifiers." 174 175 filename = self.get_object_in_store(user, "objects") 176 if not filename or not exists(filename): 177 return None 178 179 return [name for name in listdir(filename) if isfile(join(filename, name))] 180 181 def get_all_events(self, user): 182 183 "Return a set of (uid, recurrenceid) tuples for all events." 184 185 uids = self.get_events(user) 186 187 all_events = set() 188 for uid in uids: 189 all_events.add((uid, None)) 190 all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)]) 191 192 return all_events 193 194 def get_active_events(self, user): 195 196 "Return a set of uncancelled events of the form (uid, recurrenceid)." 197 198 all_events = self.get_all_events(user) 199 200 # Filter out cancelled events. 201 202 cancelled = self.get_cancellations(user) or [] 203 all_events.difference_update(cancelled) 204 return all_events 205 206 def get_event(self, user, uid, recurrenceid=None): 207 208 """ 209 Get the event for the given 'user' with the given 'uid'. If 210 the optional 'recurrenceid' is specified, a specific instance or 211 occurrence of an event is returned. 212 """ 213 214 if recurrenceid: 215 return self.get_recurrence(user, uid, recurrenceid) 216 else: 217 return self.get_complete_event(user, uid) 218 219 def get_complete_event(self, user, uid): 220 221 "Get the event for the given 'user' with the given 'uid'." 222 223 filename = self.get_object_in_store(user, "objects", uid) 224 if not filename or not exists(filename): 225 return None 226 227 return self._get_object(user, filename) 228 229 def set_event(self, user, uid, recurrenceid, node): 230 231 """ 232 Set an event for 'user' having the given 'uid' and 'recurrenceid' (which 233 if the latter is specified, a specific instance or occurrence of an 234 event is referenced), using the given 'node' description. 235 """ 236 237 if recurrenceid: 238 return self.set_recurrence(user, uid, recurrenceid, node) 239 else: 240 return self.set_complete_event(user, uid, node) 241 242 def set_complete_event(self, user, uid, node): 243 244 "Set an event for 'user' having the given 'uid' and 'node'." 245 246 filename = self.get_object_in_store(user, "objects", uid) 247 if not filename: 248 return False 249 250 return self._set_object(user, filename, node) 251 252 def remove_event(self, user, uid, recurrenceid=None): 253 254 """ 255 Remove an event for 'user' having the given 'uid'. If the optional 256 'recurrenceid' is specified, a specific instance or occurrence of an 257 event is removed. 258 """ 259 260 if recurrenceid: 261 return self.remove_recurrence(user, uid, recurrenceid) 262 else: 263 for recurrenceid in self.get_recurrences(user, uid) or []: 264 self.remove_recurrence(user, uid, recurrenceid) 265 return self.remove_complete_event(user, uid) 266 267 def remove_complete_event(self, user, uid): 268 269 "Remove an event for 'user' having the given 'uid'." 270 271 self.remove_recurrences(user, uid) 272 273 filename = self.get_object_in_store(user, "objects", uid) 274 if not filename: 275 return False 276 277 return self._remove_object(filename) 278 279 def get_recurrences(self, user, uid): 280 281 """ 282 Get additional event instances for an event of the given 'user' with the 283 indicated 'uid'. 284 """ 285 286 filename = self.get_object_in_store(user, "recurrences", uid) 287 if not filename or not exists(filename): 288 return [] 289 290 return [name for name in listdir(filename) if isfile(join(filename, name))] 291 292 def get_recurrence(self, user, uid, recurrenceid): 293 294 """ 295 For the event of the given 'user' with the given 'uid', return the 296 specific recurrence indicated by the 'recurrenceid'. 297 """ 298 299 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 300 if not filename or not exists(filename): 301 return None 302 303 return self._get_object(user, filename) 304 305 def set_recurrence(self, user, uid, recurrenceid, node): 306 307 "Set an event for 'user' having the given 'uid' and 'node'." 308 309 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 310 if not filename: 311 return False 312 313 return self._set_object(user, filename, node) 314 315 def remove_recurrence(self, user, uid, recurrenceid): 316 317 """ 318 Remove a special recurrence from an event stored by 'user' having the 319 given 'uid' and 'recurrenceid'. 320 """ 321 322 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 323 if not filename: 324 return False 325 326 return self._remove_object(filename) 327 328 def remove_recurrences(self, user, uid): 329 330 """ 331 Remove all recurrences for an event stored by 'user' having the given 332 'uid'. 333 """ 334 335 for recurrenceid in self.get_recurrences(user, uid): 336 self.remove_recurrence(user, uid, recurrenceid) 337 338 recurrences = self.get_object_in_store(user, "recurrences", uid) 339 if recurrences: 340 return self._remove_collection(recurrences) 341 342 return True 343 344 # Free/busy period providers, upon extension of the free/busy records. 345 346 def get_freebusy_providers(self, user, dt=None): 347 348 """ 349 Return a set of uncancelled events of the form (uid, recurrenceid) 350 providing free/busy details beyond the given datetime 'dt'. 351 352 If 'dt' is not specified, all events previously found to provide 353 details will be returned. Otherwise, if 'dt' is earlier than the 354 datetime recorded for the known providers, None is returned, indicating 355 that the list of providers must be recomputed. 356 """ 357 358 filename = self.get_object_in_store(user, "freebusy-providers") 359 if not filename or not exists(filename): 360 return None 361 else: 362 # Attempt to read providers, with a declaration of the datetime 363 # from which such providers are considered as still being active. 364 365 t = self._get_table(user, filename, [(1, None)]) 366 try: 367 dt_string = t[0][0] 368 except IndexError: 369 return None 370 371 # If the requested datetime is earlier than the stated datetime, the 372 # providers will need to be recomputed. 373 374 if dt: 375 providers_dt = get_datetime(dt_string) 376 if not providers_dt or providers_dt > dt: 377 return None 378 379 # Otherwise, return the providers. 380 381 return t[1:] 382 383 def set_freebusy_providers(self, user, dt, providers): 384 385 """ 386 Define the uncancelled events providing free/busy details beyond the 387 given datetime 'dt'. 388 """ 389 390 t = [(format_datetime(dt),)] 391 392 for obj in providers: 393 t.append((obj.get_uid(), obj.get_recurrenceid() or "")) 394 395 filename = self.get_object_in_store(user, "freebusy-providers") 396 if not filename: 397 return False 398 399 self._set_table(user, filename, t) 400 return True 401 402 # Free/busy period access. 403 404 def get_freebusy(self, user): 405 406 "Get free/busy details for the given 'user'." 407 408 filename = self.get_object_in_store(user, "freebusy") 409 if not filename or not exists(filename): 410 return [] 411 else: 412 return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) 413 414 def get_freebusy_for_other(self, user, other): 415 416 "For the given 'user', get free/busy details for the 'other' user." 417 418 filename = self.get_object_in_store(user, "freebusy-other", other) 419 if not filename or not exists(filename): 420 return [] 421 else: 422 return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) 423 424 def set_freebusy(self, user, freebusy): 425 426 "For the given 'user', set 'freebusy' details." 427 428 filename = self.get_object_in_store(user, "freebusy") 429 if not filename: 430 return False 431 432 self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 433 return True 434 435 def set_freebusy_for_other(self, user, freebusy, other): 436 437 "For the given 'user', set 'freebusy' details for the 'other' user." 438 439 filename = self.get_object_in_store(user, "freebusy-other", other) 440 if not filename: 441 return False 442 443 self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 444 return True 445 446 # Object status details access. 447 448 def _get_requests(self, user, queue): 449 450 "Get requests for the given 'user' from the given 'queue'." 451 452 filename = self.get_object_in_store(user, queue) 453 if not filename or not exists(filename): 454 return None 455 456 return self._get_table(user, filename, [(1, None)]) 457 458 def get_requests(self, user): 459 460 "Get requests for the given 'user'." 461 462 return self._get_requests(user, "requests") 463 464 def get_cancellations(self, user): 465 466 "Get cancellations for the given 'user'." 467 468 return self._get_requests(user, "cancellations") 469 470 def _set_requests(self, user, requests, queue): 471 472 """ 473 For the given 'user', set the list of queued 'requests' in the given 474 'queue'. 475 """ 476 477 filename = self.get_object_in_store(user, queue) 478 if not filename: 479 return False 480 481 self.acquire_lock(user) 482 try: 483 f = open(filename, "w") 484 try: 485 for request in requests: 486 print >>f, "\t".join([value or "" for value in request]) 487 finally: 488 f.close() 489 fix_permissions(filename) 490 finally: 491 self.release_lock(user) 492 493 return True 494 495 def set_requests(self, user, requests): 496 497 "For the given 'user', set the list of queued 'requests'." 498 499 return self._set_requests(user, requests, "requests") 500 501 def set_cancellations(self, user, cancellations): 502 503 "For the given 'user', set the list of queued 'cancellations'." 504 505 return self._set_requests(user, cancellations, "cancellations") 506 507 def _set_request(self, user, uid, recurrenceid, queue): 508 509 """ 510 For the given 'user', set the queued 'uid' and 'recurrenceid' in the 511 given 'queue'. 512 """ 513 514 filename = self.get_object_in_store(user, queue) 515 if not filename: 516 return False 517 518 self.acquire_lock(user) 519 try: 520 f = open(filename, "a") 521 try: 522 print >>f, "\t".join([uid, recurrenceid or ""]) 523 finally: 524 f.close() 525 fix_permissions(filename) 526 finally: 527 self.release_lock(user) 528 529 return True 530 531 def set_request(self, user, uid, recurrenceid=None): 532 533 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 534 535 return self._set_request(user, uid, recurrenceid, "requests") 536 537 def set_cancellation(self, user, uid, recurrenceid=None): 538 539 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 540 541 return self._set_request(user, uid, recurrenceid, "cancellations") 542 543 def queue_request(self, user, uid, recurrenceid=None): 544 545 """ 546 Queue a request for 'user' having the given 'uid'. If the optional 547 'recurrenceid' is specified, the request refers to a specific instance 548 or occurrence of an event. 549 """ 550 551 requests = self.get_requests(user) or [] 552 553 if (uid, recurrenceid) not in requests: 554 return self.set_request(user, uid, recurrenceid) 555 556 return False 557 558 def dequeue_request(self, user, uid, recurrenceid=None): 559 560 """ 561 Dequeue a request for 'user' having the given 'uid'. If the optional 562 'recurrenceid' is specified, the request refers to a specific instance 563 or occurrence of an event. 564 """ 565 566 requests = self.get_requests(user) or [] 567 568 try: 569 requests.remove((uid, recurrenceid)) 570 self.set_requests(user, requests) 571 except ValueError: 572 return False 573 else: 574 return True 575 576 def cancel_event(self, user, uid, recurrenceid=None): 577 578 """ 579 Queue an event for cancellation for 'user' having the given 'uid'. If 580 the optional 'recurrenceid' is specified, a specific instance or 581 occurrence of an event is cancelled. 582 """ 583 584 cancellations = self.get_cancellations(user) or [] 585 586 if (uid, recurrenceid) not in cancellations: 587 return self.set_cancellation(user, uid, recurrenceid) 588 589 return False 590 591 class FilePublisher(FileBase): 592 593 "A publisher of objects." 594 595 def __init__(self, store_dir=None): 596 FileBase.__init__(self, store_dir or PUBLISH_DIR) 597 598 def set_freebusy(self, user, freebusy): 599 600 "For the given 'user', set 'freebusy' details." 601 602 filename = self.get_object_in_store(user, "freebusy") 603 if not filename: 604 return False 605 606 record = [] 607 rwrite = record.append 608 609 rwrite(("ORGANIZER", {}, user)) 610 rwrite(("UID", {}, user)) 611 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 612 613 for fb in freebusy: 614 if not fb.transp or fb.transp == "OPAQUE": 615 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 616 map(format_datetime, [fb.get_start_point(), fb.get_end_point()])))) 617 618 f = open(filename, "wb") 619 try: 620 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 621 finally: 622 f.close() 623 fix_permissions(filename) 624 625 return True 626 627 # vim: tabstop=4 expandtab shiftwidth=4