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=STORE_DIR): 38 FileBase.__init__(self, 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 def _set_defaults(self, t, empty_defaults): 47 for i, default in empty_defaults: 48 if i >= len(t): 49 t += [None] * (i - len(t) + 1) 50 if not t[i]: 51 t[i] = default 52 return t 53 54 def _get_table(self, user, filename, empty_defaults=None): 55 56 """ 57 From the file for the given 'user' having the given 'filename', return 58 a list of tuples representing the file's contents. 59 60 The 'empty_defaults' is a list of (index, value) tuples indicating the 61 default value where a column either does not exist or provides an empty 62 value. 63 """ 64 65 self.acquire_lock(user) 66 try: 67 f = codecs.open(filename, "rb", encoding="utf-8") 68 try: 69 l = [] 70 for line in f.readlines(): 71 t = line.strip().split("\t") 72 if empty_defaults: 73 t = self._set_defaults(t, empty_defaults) 74 l.append(tuple(t)) 75 return l 76 finally: 77 f.close() 78 finally: 79 self.release_lock(user) 80 81 def _set_table(self, user, filename, items, empty_defaults=None): 82 83 """ 84 For the given 'user', write to the file having the given 'filename' the 85 'items'. 86 87 The 'empty_defaults' is a list of (index, value) tuples indicating the 88 default value where a column either does not exist or provides an empty 89 value. 90 """ 91 92 self.acquire_lock(user) 93 try: 94 f = codecs.open(filename, "wb", encoding="utf-8") 95 try: 96 for item in items: 97 if empty_defaults: 98 item = self._set_defaults(list(item), empty_defaults) 99 f.write("\t".join(item) + "\n") 100 finally: 101 f.close() 102 fix_permissions(filename) 103 finally: 104 self.release_lock(user) 105 106 def _get_object(self, user, filename): 107 108 """ 109 Return the parsed object for the given 'user' having the given 110 'filename'. 111 """ 112 113 self.acquire_lock(user) 114 try: 115 f = open(filename, "rb") 116 try: 117 return parse_object(f, "utf-8") 118 finally: 119 f.close() 120 finally: 121 self.release_lock(user) 122 123 def _set_object(self, user, filename, node): 124 125 """ 126 Set an object for the given 'user' having the given 'filename', using 127 'node' to define the object. 128 """ 129 130 self.acquire_lock(user) 131 try: 132 f = open(filename, "wb") 133 try: 134 to_stream(f, node) 135 finally: 136 f.close() 137 fix_permissions(filename) 138 finally: 139 self.release_lock(user) 140 141 return True 142 143 def _remove_object(self, filename): 144 145 "Remove the object with the given 'filename'." 146 147 try: 148 remove(filename) 149 except OSError: 150 return False 151 152 return True 153 154 def _remove_collection(self, filename): 155 156 "Remove the collection with the given 'filename'." 157 158 try: 159 rmdir(filename) 160 except OSError: 161 return False 162 163 return True 164 165 def get_events(self, user): 166 167 "Return a list of event identifiers." 168 169 filename = self.get_object_in_store(user, "objects") 170 if not filename or not exists(filename): 171 return None 172 173 return [name for name in listdir(filename) if isfile(join(filename, name))] 174 175 def get_event(self, user, uid, recurrenceid=None): 176 177 """ 178 Get the event for the given 'user' with the given 'uid'. If 179 the optional 'recurrenceid' is specified, a specific instance or 180 occurrence of an event is returned. 181 """ 182 183 if recurrenceid: 184 return self.get_recurrence(user, uid, recurrenceid) 185 else: 186 return self.get_complete_event(user, uid) 187 188 def get_complete_event(self, user, uid): 189 190 "Get the event for the given 'user' with the given 'uid'." 191 192 filename = self.get_object_in_store(user, "objects", uid) 193 if not filename or not exists(filename): 194 return None 195 196 return self._get_object(user, filename) 197 198 def set_event(self, user, uid, recurrenceid, node): 199 200 """ 201 Set an event for 'user' having the given 'uid' and 'recurrenceid' (which 202 if the latter is specified, a specific instance or occurrence of an 203 event is referenced), using the given 'node' description. 204 """ 205 206 if recurrenceid: 207 return self.set_recurrence(user, uid, recurrenceid, node) 208 else: 209 return self.set_complete_event(user, uid, node) 210 211 def set_complete_event(self, user, uid, node): 212 213 "Set an event for 'user' having the given 'uid' and 'node'." 214 215 filename = self.get_object_in_store(user, "objects", uid) 216 if not filename: 217 return False 218 219 return self._set_object(user, filename, node) 220 221 def remove_event(self, user, uid, recurrenceid=None): 222 223 """ 224 Remove an event for 'user' having the given 'uid'. If the optional 225 'recurrenceid' is specified, a specific instance or occurrence of an 226 event is removed. 227 """ 228 229 if recurrenceid: 230 return self.remove_recurrence(user, uid, recurrenceid) 231 else: 232 for recurrenceid in self.get_recurrences(user, uid) or []: 233 self.remove_recurrence(user, uid, recurrenceid) 234 return self.remove_complete_event(user, uid) 235 236 def remove_complete_event(self, user, uid): 237 238 "Remove an event for 'user' having the given 'uid'." 239 240 self.remove_recurrences(user, uid) 241 242 filename = self.get_object_in_store(user, "objects", uid) 243 if not filename: 244 return False 245 246 return self._remove_object(filename) 247 248 def get_recurrences(self, user, uid): 249 250 """ 251 Get additional event instances for an event of the given 'user' with the 252 indicated 'uid'. 253 """ 254 255 filename = self.get_object_in_store(user, "recurrences", uid) 256 if not filename or not exists(filename): 257 return [] 258 259 return [name for name in listdir(filename) if isfile(join(filename, name))] 260 261 def get_recurrence(self, user, uid, recurrenceid): 262 263 """ 264 For the event of the given 'user' with the given 'uid', return the 265 specific recurrence indicated by the 'recurrenceid'. 266 """ 267 268 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 269 if not filename or not exists(filename): 270 return None 271 272 return self._get_object(user, filename) 273 274 def set_recurrence(self, user, uid, recurrenceid, node): 275 276 "Set an event for 'user' having the given 'uid' and 'node'." 277 278 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 279 if not filename: 280 return False 281 282 return self._set_object(user, filename, node) 283 284 def remove_recurrence(self, user, uid, recurrenceid): 285 286 """ 287 Remove a special recurrence from an event stored by 'user' having the 288 given 'uid' and 'recurrenceid'. 289 """ 290 291 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 292 if not filename: 293 return False 294 295 return self._remove_object(filename) 296 297 def remove_recurrences(self, user, uid): 298 299 """ 300 Remove all recurrences for an event stored by 'user' having the given 301 'uid'. 302 """ 303 304 for recurrenceid in self.get_recurrences(user, uid): 305 self.remove_recurrence(user, uid, recurrenceid) 306 307 recurrences = self.get_object_in_store(user, "recurrences", uid) 308 if recurrences: 309 return self._remove_collection(recurrences) 310 311 return True 312 313 def get_freebusy(self, user): 314 315 "Get free/busy details for the given 'user'." 316 317 filename = self.get_object_in_store(user, "freebusy") 318 if not filename or not exists(filename): 319 return [] 320 else: 321 return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) 322 323 def get_freebusy_for_other(self, user, other): 324 325 "For the given 'user', get free/busy details for the 'other' user." 326 327 filename = self.get_object_in_store(user, "freebusy-other", other) 328 if not filename or not exists(filename): 329 return [] 330 else: 331 return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) 332 333 def set_freebusy(self, user, freebusy): 334 335 "For the given 'user', set 'freebusy' details." 336 337 filename = self.get_object_in_store(user, "freebusy") 338 if not filename: 339 return False 340 341 self._set_table(user, filename, map(lambda fb: fb.as_tuple(), freebusy), 342 [(3, "OPAQUE"), (4, "")]) 343 return True 344 345 def set_freebusy_for_other(self, user, freebusy, other): 346 347 "For the given 'user', set 'freebusy' details for the 'other' user." 348 349 filename = self.get_object_in_store(user, "freebusy-other", other) 350 if not filename: 351 return False 352 353 self._set_table(user, filename, map(lambda fb: fb.as_tuple(), freebusy), 354 [(2, ""), (3, "OPAQUE"), (4, ""), (5, ""), (6, "")]) 355 return True 356 357 def _get_requests(self, user, queue): 358 359 "Get requests for the given 'user' from the given 'queue'." 360 361 filename = self.get_object_in_store(user, queue) 362 if not filename or not exists(filename): 363 return None 364 365 return self._get_table(user, filename, [(1, None)]) 366 367 def get_requests(self, user): 368 369 "Get requests for the given 'user'." 370 371 return self._get_requests(user, "requests") 372 373 def get_cancellations(self, user): 374 375 "Get cancellations for the given 'user'." 376 377 return self._get_requests(user, "cancellations") 378 379 def _set_requests(self, user, requests, queue): 380 381 """ 382 For the given 'user', set the list of queued 'requests' in the given 383 'queue'. 384 """ 385 386 filename = self.get_object_in_store(user, queue) 387 if not filename: 388 return False 389 390 self.acquire_lock(user) 391 try: 392 f = open(filename, "w") 393 try: 394 for request in requests: 395 print >>f, "\t".join([value or "" for value in request]) 396 finally: 397 f.close() 398 fix_permissions(filename) 399 finally: 400 self.release_lock(user) 401 402 return True 403 404 def set_requests(self, user, requests): 405 406 "For the given 'user', set the list of queued 'requests'." 407 408 return self._set_requests(user, requests, "requests") 409 410 def set_cancellations(self, user, cancellations): 411 412 "For the given 'user', set the list of queued 'cancellations'." 413 414 return self._set_requests(user, cancellations, "cancellations") 415 416 def _set_request(self, user, uid, recurrenceid, queue): 417 418 """ 419 For the given 'user', set the queued 'uid' and 'recurrenceid' in the 420 given 'queue'. 421 """ 422 423 filename = self.get_object_in_store(user, queue) 424 if not filename: 425 return False 426 427 self.acquire_lock(user) 428 try: 429 f = open(filename, "a") 430 try: 431 print >>f, "\t".join([uid, recurrenceid or ""]) 432 finally: 433 f.close() 434 fix_permissions(filename) 435 finally: 436 self.release_lock(user) 437 438 return True 439 440 def set_request(self, user, uid, recurrenceid=None): 441 442 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 443 444 return self._set_request(user, uid, recurrenceid, "requests") 445 446 def set_cancellation(self, user, uid, recurrenceid=None): 447 448 "For the given 'user', set the queued 'uid' and 'recurrenceid'." 449 450 return self._set_request(user, uid, recurrenceid, "cancellations") 451 452 def queue_request(self, user, uid, recurrenceid=None): 453 454 """ 455 Queue a request for 'user' having the given 'uid'. If the optional 456 'recurrenceid' is specified, the request refers to a specific instance 457 or occurrence of an event. 458 """ 459 460 requests = self.get_requests(user) or [] 461 462 if (uid, recurrenceid) not in requests: 463 return self.set_request(user, uid, recurrenceid) 464 465 return False 466 467 def dequeue_request(self, user, uid, recurrenceid=None): 468 469 """ 470 Dequeue a request for 'user' having the given 'uid'. If the optional 471 'recurrenceid' is specified, the request refers to a specific instance 472 or occurrence of an event. 473 """ 474 475 requests = self.get_requests(user) or [] 476 477 try: 478 requests.remove((uid, recurrenceid)) 479 self.set_requests(user, requests) 480 except ValueError: 481 return False 482 else: 483 return True 484 485 def cancel_event(self, user, uid, recurrenceid=None): 486 487 """ 488 Queue an event for cancellation for 'user' having the given 'uid'. If 489 the optional 'recurrenceid' is specified, a specific instance or 490 occurrence of an event is cancelled. 491 """ 492 493 cancellations = self.get_cancellations(user) or [] 494 495 if (uid, recurrenceid) not in cancellations: 496 return self.set_cancellation(user, uid, recurrenceid) 497 498 return False 499 500 class FilePublisher(FileBase): 501 502 "A publisher of objects." 503 504 def __init__(self, store_dir=PUBLISH_DIR): 505 FileBase.__init__(self, store_dir) 506 507 def set_freebusy(self, user, freebusy): 508 509 "For the given 'user', set 'freebusy' details." 510 511 filename = self.get_object_in_store(user, "freebusy") 512 if not filename: 513 return False 514 515 record = [] 516 rwrite = record.append 517 518 rwrite(("ORGANIZER", {}, user)) 519 rwrite(("UID", {}, user)) 520 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 521 522 for fb in freebusy: 523 if not fb.transp or fb.transp == "OPAQUE": 524 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 525 map(format_datetime, [fb.get_start(), fb.get_end()])))) 526 527 f = open(filename, "wb") 528 try: 529 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 530 finally: 531 f.close() 532 fix_permissions(filename) 533 534 return True 535 536 # vim: tabstop=4 expandtab shiftwidth=4