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