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 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 _get_object(self, user, filename): 44 45 """ 46 Return the parsed object for the given 'user' having the given 47 'filename'. 48 """ 49 50 self.acquire_lock(user) 51 try: 52 f = open(filename, "rb") 53 try: 54 return parse_object(f, "utf-8") 55 finally: 56 f.close() 57 finally: 58 self.release_lock(user) 59 60 def _set_object(self, user, filename, node): 61 62 """ 63 Set an object for the given 'user' having the given 'filename', using 64 'node' to define the object. 65 """ 66 67 self.acquire_lock(user) 68 try: 69 f = open(filename, "wb") 70 try: 71 to_stream(f, node) 72 finally: 73 f.close() 74 fix_permissions(filename) 75 finally: 76 self.release_lock(user) 77 78 return True 79 80 def _remove_object(self, filename): 81 82 "Remove the object with the given 'filename'." 83 84 try: 85 remove(filename) 86 except OSError: 87 return False 88 89 return True 90 91 def get_events(self, user): 92 93 "Return a list of event identifiers." 94 95 filename = self.get_object_in_store(user, "objects") 96 if not filename or not exists(filename): 97 return None 98 99 return [name for name in listdir(filename) if isfile(join(filename, name))] 100 101 def get_event(self, user, uid): 102 103 "Get the event for the given 'user' with the given 'uid'." 104 105 filename = self.get_object_in_store(user, "objects", uid) 106 if not filename or not exists(filename): 107 return None 108 109 return self._get_object(user, filename) 110 111 def set_event(self, user, uid, node): 112 113 "Set an event for 'user' having the given 'uid' and 'node'." 114 115 filename = self.get_object_in_store(user, "objects", uid) 116 if not filename: 117 return False 118 119 return self._set_object(user, filename, node) 120 121 def remove_event(self, user, uid): 122 123 "Remove an event for 'user' having the given 'uid'." 124 125 filename = self.get_object_in_store(user, "objects", uid) 126 if not filename: 127 return False 128 129 return self._remove_object(filename) 130 131 def get_recurrences(self, user, uid): 132 133 """ 134 Get additional event instances for an event of the given 'user' with the 135 indicated 'uid'. 136 """ 137 138 filename = self.get_object_in_store(user, "recurrences", uid) 139 if not filename or not exists(filename): 140 return None 141 142 return [name for name in listdir(filename) if isfile(join(filename, name))] 143 144 def get_recurrence(self, user, uid, recurrenceid): 145 146 """ 147 For the event of the given 'user' with the given 'uid', return the 148 specific recurrence indicated by the 'recurrenceid'. 149 """ 150 151 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 152 if not filename or not exists(filename): 153 return None 154 155 return self._get_object(user, filename) 156 157 def set_recurrence(self, user, uid, recurrenceid, node): 158 159 "Set an event for 'user' having the given 'uid' and 'node'." 160 161 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 162 if not filename: 163 return False 164 165 return self._set_object(user, filename, node) 166 167 def remove_recurrence(self, user, uid, recurrenceid): 168 169 "Remove an event for 'user' having the given 'uid'." 170 171 filename = self.get_object_in_store(user, "objects", uid) 172 if not filename: 173 return False 174 175 return self._remove_object(filename) 176 177 def get_freebusy(self, user): 178 179 "Get free/busy details for the given 'user'." 180 181 filename = self.get_object_in_store(user, "freebusy") 182 if not filename or not exists(filename): 183 return [] 184 else: 185 return self._get_freebusy(user, filename) 186 187 def get_freebusy_for_other(self, user, other): 188 189 "For the given 'user', get free/busy details for the 'other' user." 190 191 filename = self.get_object_in_store(user, "freebusy-other", other) 192 if not filename or not exists(filename): 193 return [] 194 else: 195 return self._get_freebusy(user, filename) 196 197 def _get_freebusy(self, user, filename): 198 199 "For the given 'user', get the free/busy details from 'filename'." 200 201 self.acquire_lock(user) 202 try: 203 f = open(filename) 204 try: 205 l = [] 206 for line in f.readlines(): 207 l.append(tuple(line.strip().split("\t"))) 208 return l 209 finally: 210 f.close() 211 finally: 212 self.release_lock(user) 213 214 def set_freebusy(self, user, freebusy): 215 216 "For the given 'user', set 'freebusy' details." 217 218 filename = self.get_object_in_store(user, "freebusy") 219 if not filename: 220 return False 221 222 self._set_freebusy(user, filename, freebusy) 223 return True 224 225 def set_freebusy_for_other(self, user, freebusy, other): 226 227 "For the given 'user', set 'freebusy' details for the 'other' user." 228 229 filename = self.get_object_in_store(user, "freebusy-other", other) 230 if not filename: 231 return False 232 233 self._set_freebusy(user, filename, freebusy) 234 return True 235 236 def _set_freebusy(self, user, filename, freebusy): 237 238 """ 239 For the given 'user', write to the file having the given 'filename' the 240 'freebusy' details. 241 """ 242 243 self.acquire_lock(user) 244 try: 245 f = open(filename, "w") 246 try: 247 for item in freebusy: 248 f.write("\t".join([(value or "OPAQUE") for value in item]) + "\n") 249 finally: 250 f.close() 251 fix_permissions(filename) 252 finally: 253 self.release_lock(user) 254 255 def _get_requests(self, user, queue): 256 257 "Get requests for the given 'user' from the given 'queue'." 258 259 filename = self.get_object_in_store(user, queue) 260 if not filename or not exists(filename): 261 return None 262 263 self.acquire_lock(user) 264 try: 265 f = open(filename) 266 try: 267 return [line.strip() for line in f.readlines()] 268 finally: 269 f.close() 270 finally: 271 self.release_lock(user) 272 273 def get_requests(self, user): 274 275 "Get requests for the given 'user'." 276 277 return self._get_requests(user, "requests") 278 279 def get_cancellations(self, user): 280 281 "Get cancellations for the given 'user'." 282 283 return self._get_requests(user, "cancellations") 284 285 def _set_requests(self, user, requests, queue): 286 287 """ 288 For the given 'user', set the list of queued 'requests' in the given 289 'queue'. 290 """ 291 292 filename = self.get_object_in_store(user, queue) 293 if not filename: 294 return False 295 296 self.acquire_lock(user) 297 try: 298 f = open(filename, "w") 299 try: 300 for request in requests: 301 print >>f, request 302 finally: 303 f.close() 304 fix_permissions(filename) 305 finally: 306 self.release_lock(user) 307 308 return True 309 310 def set_requests(self, user, requests): 311 312 "For the given 'user', set the list of queued 'requests'." 313 314 return self._set_requests(user, requests, "requests") 315 316 def set_cancellations(self, user, cancellations): 317 318 "For the given 'user', set the list of queued 'cancellations'." 319 320 return self._set_requests(user, cancellations, "cancellations") 321 322 def _set_request(self, user, request, queue): 323 324 "For the given 'user', set the queued 'request' in the given 'queue'." 325 326 filename = self.get_object_in_store(user, queue) 327 if not filename: 328 return False 329 330 self.acquire_lock(user) 331 try: 332 f = open(filename, "a") 333 try: 334 print >>f, request 335 finally: 336 f.close() 337 fix_permissions(filename) 338 finally: 339 self.release_lock(user) 340 341 return True 342 343 def set_request(self, user, request): 344 345 "For the given 'user', set the queued 'request'." 346 347 return self._set_request(user, request, "requests") 348 349 def set_cancellation(self, user, cancellation): 350 351 "For the given 'user', set the queued 'cancellation'." 352 353 return self._set_request(user, cancellation, "cancellations") 354 355 def queue_request(self, user, uid): 356 357 "Queue a request for 'user' having the given 'uid'." 358 359 requests = self.get_requests(user) or [] 360 361 if uid not in requests: 362 return self.set_request(user, uid) 363 364 return False 365 366 def dequeue_request(self, user, uid): 367 368 "Dequeue a request for 'user' having the given 'uid'." 369 370 requests = self.get_requests(user) or [] 371 372 try: 373 requests.remove(uid) 374 self.set_requests(user, requests) 375 except ValueError: 376 return False 377 else: 378 return True 379 380 def cancel_event(self, user, uid): 381 382 "Queue an event for cancellation for 'user' having the given 'uid'." 383 384 cancellations = self.get_cancellations(user) or [] 385 386 if uid not in cancellations: 387 return self.set_cancellation(user, uid) 388 389 return False 390 391 class FilePublisher(FileBase): 392 393 "A publisher of objects." 394 395 def __init__(self, store_dir=PUBLISH_DIR): 396 FileBase.__init__(self, store_dir) 397 398 def set_freebusy(self, user, freebusy): 399 400 "For the given 'user', set 'freebusy' details." 401 402 filename = self.get_object_in_store(user, "freebusy") 403 if not filename: 404 return False 405 406 record = [] 407 rwrite = record.append 408 409 rwrite(("ORGANIZER", {}, user)) 410 rwrite(("UID", {}, user)) 411 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 412 413 for start, end, uid, transp in freebusy: 414 if not transp or transp == "OPAQUE": 415 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) 416 417 f = open(filename, "w") 418 try: 419 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 420 finally: 421 f.close() 422 fix_permissions(filename) 423 424 return True 425 426 # vim: tabstop=4 expandtab shiftwidth=4