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 353 filename = self.get_object_in_store(user, "freebusy-providers") 354 if not filename or not exists(filename): 355 return None 356 else: 357 # Attempt to read providers, with a declaration of the datetime 358 # from which such providers are considered as still being active. 359 360 t = self._get_table(user, filename, [(1, None)]) 361 try: 362 dt_string = t[0][0] 363 except IndexError: 364 return None 365 366 # If the requested datetime is earlier than the stated datetime, the 367 # providers will need to be recomputed. 368 369 if dt: 370 providers_dt = get_datetime(dt_string) 371 if not providers_dt or providers_dt > dt: 372 return None 373 374 # Otherwise, return the providers. 375 376 return t[1:] 377 378 # Free/busy period access. 379 380 def get_freebusy(self, user): 381 382 "Get free/busy details for the given 'user'." 383 384 filename = self.get_object_in_store(user, "freebusy") 385 if not filename or not exists(filename): 386 return [] 387 else: 388 return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) 389 390 def get_freebusy_for_other(self, user, other): 391 392 "For the given 'user', get free/busy details for the 'other' user." 393 394 filename = self.get_object_in_store(user, "freebusy-other", other) 395 if not filename or not exists(filename): 396 return [] 397 else: 398 return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) 399 400 def set_freebusy(self, user, freebusy): 401 402 "For the given 'user', set 'freebusy' details." 403 404 filename = self.get_object_in_store(user, "freebusy") 405 if not filename: 406 return False 407 408 self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 409 return True 410 411 def set_freebusy_for_other(self, user, freebusy, other): 412 413 "For the given 'user', set 'freebusy' details for the 'other' user." 414 415 filename = self.get_object_in_store(user, "freebusy-other", other) 416 if not filename: 417 return False 418 419 self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) 420 return True 421 422 # Object status details access. 423 424 def _get_requests(self, user, queue): 425 426 "Get requests for the given 'user' from the given 'queue'." 427 428 filename = self.get_object_in_store(user, queue) 429 if not filename or not exists(filename): 430 return None 431 432 return self._get_table(user, filename, [(1, None)]) 433 434 def get_requests(self, user): 435 436 "Get requests for the given 'user'." 437 438 return self._get_requests(user, "requests") 439 440 def get_cancellations(self, user): 441 442 "Get cancellations for the given 'user'." 443 444 return self._get_requests(user, "cancellations") 445 446 def _set_requests(self, user, requests, queue): 447 448 """ 449 For the given 'user', set the list of queued 'requests' in the given 450 'queue'. 451 """ 452 453 filename = self.get_object_in_store(user, queue) 454 if not filename: 455 return False 456 457 self.acquire_lock(user) 458 try: 459 f = open(filename, "w") 460 try: 461 for request in requests: 462 print >>f, "\t".join([value or "" for value in request]) 463 finally: 464 f.close() 465 fix_permissions(filename) 466 finally: 467 self.release_lock(user) 468 469 return True 470 471 def set_requests(self, user, requests): 472 473 "For the given 'user', set the list of queued 'requests'." 474 475 return self._set_requests(user, requests, "requests") 476 477 def set_cancellations(self, user, cancellations): 478 479 "For the given 'user', set the list of queued 'cancellations'." 480 481 return self._set_requests(user, cancellations, "cancellations") 482 483 def _set_request(self, user, uid, recurrenceid, queue): 484 485 """ 486 For the given 'user', set the queued 'uid' and 'recurrenceid' in the 487 given 'queue'. 488 """ 489 490 filename = self.get_object_in_store(user, queue) 491 if not filename: 492 return False 493 494 self.acquire_lock(user) 495 try: 496 f = open(filename, "a") 497 try: 498 print >>f, "\t".join([uid, recurrenceid or ""]) 499 finally: 500 f.close() 501 fix_permissions(filename) 502 finally: 503 self.release_lock(user) 504 505 return True 506 507 def set_request(self, user, uid, recurrenceid=None): 508 509 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 510 511 return self._set_request(user, uid, recurrenceid, "requests") 512 513 def set_cancellation(self, user, uid, recurrenceid=None): 514 515 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 516 517 return self._set_request(user, uid, recurrenceid, "cancellations") 518 519 def queue_request(self, user, uid, recurrenceid=None): 520 521 """ 522 Queue a request for 'user' having the given 'uid'. If the optional 523 'recurrenceid' is specified, the request refers to a specific instance 524 or occurrence of an event. 525 """ 526 527 requests = self.get_requests(user) or [] 528 529 if (uid, recurrenceid) not in requests: 530 return self.set_request(user, uid, recurrenceid) 531 532 return False 533 534 def dequeue_request(self, user, uid, recurrenceid=None): 535 536 """ 537 Dequeue a request for 'user' having the given 'uid'. If the optional 538 'recurrenceid' is specified, the request refers to a specific instance 539 or occurrence of an event. 540 """ 541 542 requests = self.get_requests(user) or [] 543 544 try: 545 requests.remove((uid, recurrenceid)) 546 self.set_requests(user, requests) 547 except ValueError: 548 return False 549 else: 550 return True 551 552 def cancel_event(self, user, uid, recurrenceid=None): 553 554 """ 555 Queue an event for cancellation for 'user' having the given 'uid'. If 556 the optional 'recurrenceid' is specified, a specific instance or 557 occurrence of an event is cancelled. 558 """ 559 560 cancellations = self.get_cancellations(user) or [] 561 562 if (uid, recurrenceid) not in cancellations: 563 return self.set_cancellation(user, uid, recurrenceid) 564 565 return False 566 567 class FilePublisher(FileBase): 568 569 "A publisher of objects." 570 571 def __init__(self, store_dir=None): 572 FileBase.__init__(self, store_dir or PUBLISH_DIR) 573 574 def set_freebusy(self, user, freebusy): 575 576 "For the given 'user', set 'freebusy' details." 577 578 filename = self.get_object_in_store(user, "freebusy") 579 if not filename: 580 return False 581 582 record = [] 583 rwrite = record.append 584 585 rwrite(("ORGANIZER", {}, user)) 586 rwrite(("UID", {}, user)) 587 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 588 589 for fb in freebusy: 590 if not fb.transp or fb.transp == "OPAQUE": 591 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 592 map(format_datetime, [fb.get_start_point(), fb.get_end_point()])))) 593 594 f = open(filename, "wb") 595 try: 596 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 597 finally: 598 f.close() 599 fix_permissions(filename) 600 601 return True 602 603 # vim: tabstop=4 expandtab shiftwidth=4