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