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