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