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