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_freebusy(self, user): 132 133 "Get free/busy details for the given 'user'." 134 135 filename = self.get_object_in_store(user, "freebusy") 136 if not filename or not exists(filename): 137 return [] 138 else: 139 return self._get_freebusy(user, filename) 140 141 def get_freebusy_for_other(self, user, other): 142 143 "For the given 'user', get free/busy details for the 'other' user." 144 145 filename = self.get_object_in_store(user, "freebusy-other", other) 146 if not filename or not exists(filename): 147 return [] 148 else: 149 return self._get_freebusy(user, filename) 150 151 def _get_freebusy(self, user, filename): 152 153 "For the given 'user', get the free/busy details from 'filename'." 154 155 self.acquire_lock(user) 156 try: 157 f = open(filename) 158 try: 159 l = [] 160 for line in f.readlines(): 161 l.append(tuple(line.strip().split("\t"))) 162 return l 163 finally: 164 f.close() 165 finally: 166 self.release_lock(user) 167 168 def set_freebusy(self, user, freebusy): 169 170 "For the given 'user', set 'freebusy' details." 171 172 filename = self.get_object_in_store(user, "freebusy") 173 if not filename: 174 return False 175 176 self._set_freebusy(user, filename, freebusy) 177 return True 178 179 def set_freebusy_for_other(self, user, freebusy, other): 180 181 "For the given 'user', set 'freebusy' details for the 'other' user." 182 183 filename = self.get_object_in_store(user, "freebusy-other", other) 184 if not filename: 185 return False 186 187 self._set_freebusy(user, filename, freebusy) 188 return True 189 190 def _set_freebusy(self, user, filename, freebusy): 191 192 """ 193 For the given 'user', write to the file having the given 'filename' the 194 'freebusy' details. 195 """ 196 197 self.acquire_lock(user) 198 try: 199 f = open(filename, "w") 200 try: 201 for item in freebusy: 202 f.write("\t".join([(value or "OPAQUE") for value in item]) + "\n") 203 finally: 204 f.close() 205 fix_permissions(filename) 206 finally: 207 self.release_lock(user) 208 209 def _get_requests(self, user, queue): 210 211 "Get requests for the given 'user' from the given 'queue'." 212 213 filename = self.get_object_in_store(user, queue) 214 if not filename or not exists(filename): 215 return None 216 217 self.acquire_lock(user) 218 try: 219 f = open(filename) 220 try: 221 return [line.strip() for line in f.readlines()] 222 finally: 223 f.close() 224 finally: 225 self.release_lock(user) 226 227 def get_requests(self, user): 228 229 "Get requests for the given 'user'." 230 231 return self._get_requests(user, "requests") 232 233 def get_cancellations(self, user): 234 235 "Get cancellations for the given 'user'." 236 237 return self._get_requests(user, "cancellations") 238 239 def _set_requests(self, user, requests, queue): 240 241 """ 242 For the given 'user', set the list of queued 'requests' in the given 243 'queue'. 244 """ 245 246 filename = self.get_object_in_store(user, queue) 247 if not filename: 248 return False 249 250 self.acquire_lock(user) 251 try: 252 f = open(filename, "w") 253 try: 254 for request in requests: 255 print >>f, request 256 finally: 257 f.close() 258 fix_permissions(filename) 259 finally: 260 self.release_lock(user) 261 262 return True 263 264 def set_requests(self, user, requests): 265 266 "For the given 'user', set the list of queued 'requests'." 267 268 return self._set_requests(user, requests, "requests") 269 270 def set_cancellations(self, user, cancellations): 271 272 "For the given 'user', set the list of queued 'cancellations'." 273 274 return self._set_requests(user, cancellations, "cancellations") 275 276 def _set_request(self, user, request, queue): 277 278 "For the given 'user', set the queued 'request' in the given 'queue'." 279 280 filename = self.get_object_in_store(user, queue) 281 if not filename: 282 return False 283 284 self.acquire_lock(user) 285 try: 286 f = open(filename, "a") 287 try: 288 print >>f, request 289 finally: 290 f.close() 291 fix_permissions(filename) 292 finally: 293 self.release_lock(user) 294 295 return True 296 297 def set_request(self, user, request): 298 299 "For the given 'user', set the queued 'request'." 300 301 return self._set_request(user, request, "requests") 302 303 def set_cancellation(self, user, cancellation): 304 305 "For the given 'user', set the queued 'cancellation'." 306 307 return self._set_request(user, cancellation, "cancellations") 308 309 def queue_request(self, user, uid): 310 311 "Queue a request for 'user' having the given 'uid'." 312 313 requests = self.get_requests(user) or [] 314 315 if uid not in requests: 316 return self.set_request(user, uid) 317 318 return False 319 320 def dequeue_request(self, user, uid): 321 322 "Dequeue a request for 'user' having the given 'uid'." 323 324 requests = self.get_requests(user) or [] 325 326 try: 327 requests.remove(uid) 328 self.set_requests(user, requests) 329 except ValueError: 330 return False 331 else: 332 return True 333 334 def cancel_event(self, user, uid): 335 336 "Queue an event for cancellation for 'user' having the given 'uid'." 337 338 cancellations = self.get_cancellations(user) or [] 339 340 if uid not in cancellations: 341 return self.set_cancellation(user, uid) 342 343 return False 344 345 class FilePublisher(FileBase): 346 347 "A publisher of objects." 348 349 def __init__(self, store_dir=PUBLISH_DIR): 350 FileBase.__init__(self, store_dir) 351 352 def set_freebusy(self, user, freebusy): 353 354 "For the given 'user', set 'freebusy' details." 355 356 filename = self.get_object_in_store(user, "freebusy") 357 if not filename: 358 return False 359 360 record = [] 361 rwrite = record.append 362 363 rwrite(("ORGANIZER", {}, user)) 364 rwrite(("UID", {}, user)) 365 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 366 367 for start, end, uid, transp in freebusy: 368 if not transp or transp == "OPAQUE": 369 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) 370 371 f = open(filename, "w") 372 try: 373 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 374 finally: 375 f.close() 376 fix_permissions(filename) 377 378 return True 379 380 # vim: tabstop=4 expandtab shiftwidth=4