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