1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - ItemSupport library 4 5 @copyright: 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from MoinMoin.Page import Page 10 from MoinMoin.PageEditor import PageEditor 11 from MoinMoin.security import Permissions 12 from MoinMoin.util import lock 13 from MoinSupport import getMetadata, getPagesForSearch 14 import os 15 16 # Content storage support. 17 18 class AdminPermissionsForPage(Permissions): 19 20 "Permit saving of ACL-enabled pages." 21 22 def __init__(self, user, pagename): 23 Permissions.__init__(self, user) 24 self.pagename = pagename 25 26 def admin(self, pagename): 27 return pagename == self.pagename 28 29 # Underlying storage mechanisms. 30 31 class GeneralItemStore: 32 33 "Common item store functionality." 34 35 def __init__(self, lock_dir): 36 37 "Initialise an item store with the given 'lock_dir' guarding access." 38 39 self.lock_dir = lock_dir 40 self.writelock = lock.WriteLock(lock_dir) 41 self.readlock = lock.ReadLock(lock_dir) 42 43 def deduce_next(self): 44 45 "Deduce the next item number from the existing item files." 46 47 return max(self.get_keys() or [-1]) + 1 48 49 # High-level methods. 50 51 def __len__(self): 52 53 """ 54 Return the number of items. 55 """ 56 57 return len(self.keys()) 58 59 def __iter__(self): 60 61 "Return an iterator over the items in the store." 62 63 return ItemIterator(self) 64 65 def keys(self): 66 67 "Return a list of keys for items in the store." 68 69 self.readlock.acquire() 70 try: 71 return self.get_keys() 72 finally: 73 self.readlock.release() 74 75 def __getitem__(self, number): 76 77 "Return the item with the given 'number'." 78 79 self.readlock.acquire() 80 try: 81 try: 82 return self.read_item(number) 83 except (IOError, OSError): 84 raise IndexError, number 85 finally: 86 self.readlock.release() 87 88 def __delitem__(self, number): 89 90 "Remove the item with the given 'number' from the store." 91 92 self.writelock.acquire() 93 try: 94 try: 95 self.remove_item(number) 96 except (IOError, OSError): 97 raise IndexError, number 98 finally: 99 self.writelock.release() 100 101 def next(self): 102 103 """ 104 Return the number of the next item (which should also be the number of 105 items if none have been deleted). 106 """ 107 108 self.writelock.acquire() 109 try: 110 return self.get_next() 111 finally: 112 self.writelock.release() 113 114 class DirectoryItemStore(GeneralItemStore): 115 116 "A directory-based item store." 117 118 def __init__(self, path, lock_dir): 119 120 "Initialise an item store for the given 'path' and 'lock_dir'." 121 122 self.path = path 123 self.next_path = os.path.join(self.path, "next") 124 self.lock_dir = lock_dir 125 self.writelock = lock.WriteLock(lock_dir) 126 self.readlock = lock.ReadLock(lock_dir) 127 128 def mtime(self): 129 130 "Return the last modified time of the item store directory." 131 132 return os.path.getmtime(self.path) 133 134 def get_next(self): 135 136 "Return the next item number." 137 138 next = self.read_next() 139 if next is None: 140 next = self.deduce_next() 141 self.write_next(next) 142 return next 143 144 def get_keys(self): 145 146 "Return the item keys." 147 148 return [int(filename) for filename in os.listdir(self.path) if filename.isdigit()] 149 150 def read_next(self): 151 152 "Read the next item number from a special file." 153 154 if not os.path.exists(self.next_path): 155 return None 156 157 f = open(self.next_path) 158 try: 159 try: 160 return int(f.read()) 161 except ValueError: 162 return None 163 finally: 164 f.close() 165 166 def write_next(self, next): 167 168 "Write the 'next' item number to a special file." 169 170 f = open(self.next_path, "w") 171 try: 172 f.write(str(next)) 173 finally: 174 f.close() 175 176 def write_item(self, item, next): 177 178 "Write the given 'item' to a file with the given 'next' item number." 179 180 f = open(self.get_item_path(next), "w") 181 try: 182 f.write(item) 183 finally: 184 f.close() 185 186 def read_item(self, number): 187 188 "Read the item with the given item 'number'." 189 190 f = open(self.get_item_path(number)) 191 try: 192 return f.read() 193 finally: 194 f.close() 195 196 def remove_item(self, number): 197 198 "Remove the item with the given item 'number'." 199 200 os.remove(self.get_item_path(number)) 201 202 def get_item_path(self, number): 203 204 "Get the path for the given item 'number'." 205 206 path = os.path.abspath(os.path.join(self.path, str(number))) 207 basepath = os.path.join(self.path, "") 208 209 if os.path.commonprefix([path, basepath]) != basepath: 210 raise OSError, path 211 212 return path 213 214 # High-level methods. 215 216 def append(self, item): 217 218 "Append the given 'item' to the store." 219 220 self.writelock.acquire() 221 try: 222 next = self.get_next() 223 self.write_item(item, next) 224 self.write_next(next + 1) 225 finally: 226 self.writelock.release() 227 228 class SubpageItemStore(GeneralItemStore): 229 230 "A subpage-based item store." 231 232 def __init__(self, page, lock_dir): 233 234 "Initialise an item store for subpages under the given 'page'." 235 236 GeneralItemStore.__init__(self, lock_dir) 237 self.page = page 238 239 def mtime(self): 240 241 "Return the last modified time of the item store." 242 243 keys = self.get_keys() 244 if not keys: 245 page = self.page 246 else: 247 page = Page(self.page.request, self.get_item_path(max(keys))) 248 249 return wikiutil.version2timestamp( 250 getMetadata(page)["last-modified"] 251 ) 252 253 def get_next(self): 254 255 "Return the next item number." 256 257 return self.deduce_next() 258 259 def get_keys(self): 260 261 "Return the item keys." 262 263 # Collect the strict subpages of the parent page. 264 265 leafnames = [] 266 parentname = self.page.page_name 267 268 for page in getPagesForSearch("title:regex:^%s/" % parentname, self.page.request): 269 basename, leafname = page.page_name.rsplit("/", 1) 270 271 # Only collect numbered pages immediately below the parent. 272 273 if basename == parentname and leafname.isdigit(): 274 leafnames.append(int(leafname)) 275 276 return leafnames 277 278 def write_item(self, item, next): 279 280 "Write the given 'item' to a file with the given 'next' item number." 281 282 request = self.page.request 283 acl = self.page.getACL(request) 284 pagename = self.get_item_path(next) 285 286 # To add a page with an ACL, a special policy is required. 287 288 may = request.user.may 289 request.user.may = AdminPermissionsForPage(request.user, pagename) 290 291 # Attempt to save the page, copying any ACL. 292 293 try: 294 page = PageEditor(request, pagename) 295 page.saveText(acl.getString() + item, 0) 296 297 # Restore the original policy. 298 299 finally: 300 request.user.may = may 301 302 def read_item(self, number): 303 304 "Read the item with the given item 'number'." 305 306 page = Page(self.page.request, self.get_item_path(number)) 307 308 # Remove any page directives. 309 310 return page.data 311 312 def remove_item(self, number): 313 314 "Remove the item with the given item 'number'." 315 316 page = PageEditor(self.page.request, self.get_item_path(number)) 317 page.deletePage() 318 319 def get_item_path(self, number): 320 321 "Get the path for the given item 'number'." 322 323 return "%s/%s" % (self.page.page_name, number) 324 325 # High-level methods. 326 327 def append(self, item): 328 329 "Append the given 'item' to the store." 330 331 self.writelock.acquire() 332 try: 333 next = self.get_next() 334 self.write_item(item, next) 335 finally: 336 self.writelock.release() 337 338 class ItemIterator: 339 340 "An iterator over items in a store." 341 342 def __init__(self, store, direction=1): 343 self.store = store 344 self.direction = direction 345 self.reset() 346 347 def reset(self): 348 if self.direction == 1: 349 self._next = 0 350 self.final = self.store.next() 351 else: 352 self._next = self.store.next() - 1 353 self.final = 0 354 355 def more(self): 356 if self.direction == 1: 357 return self._next < self.final 358 else: 359 return self._next >= self.final 360 361 def get_next(self): 362 next = self._next 363 self._next += self.direction 364 return next 365 366 def next(self): 367 while self.more(): 368 try: 369 return self.store[self.get_next()] 370 except IndexError: 371 pass 372 373 raise StopIteration 374 375 def reverse(self): 376 self.direction = -self.direction 377 self.reset() 378 379 def reversed(self): 380 self.reverse() 381 return self 382 383 def __iter__(self): 384 return self 385 386 def getDirectoryItemStoreForPage(page, item_dir, lock_dir): 387 388 """ 389 A convenience function returning a directory-based store for the given 390 'page', using the given 'item_dir' and 'lock_dir'. 391 """ 392 393 item_dir_path = tuple(item_dir.split("/")) 394 lock_dir_path = tuple(lock_dir.split("/")) 395 return DirectoryItemStore(page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path)) 396 397 def getSubpageItemStoreForPage(page, lock_dir): 398 399 """ 400 A convenience function returning a subpage-based store for the given 401 'page', using the given 'lock_dir'. 402 """ 403 404 lock_dir_path = tuple(lock_dir.split("/")) 405 return SubpageItemStore(page, page.getPagePath(*lock_dir_path)) 406 407 # Page-oriented item store classes. 408 409 class ItemStoreBase: 410 411 "Access item stores via pages, observing page access restrictions." 412 413 def __init__(self, page, store): 414 self.page = page 415 self.store = store 416 417 def can_write(self): 418 419 """ 420 Return whether the user associated with the request can write to the 421 page owning this store. 422 """ 423 424 user = self.page.request.user 425 return user and user.may.write(self.page.page_name) 426 427 def can_read(self): 428 429 """ 430 Return whether the user associated with the request can read from the 431 page owning this store. 432 """ 433 434 user = self.page.request.user 435 return user and user.may.read(self.page.page_name) 436 437 def can_delete(self): 438 439 """ 440 Return whether the user associated with the request can delete the 441 page owning this store. 442 """ 443 444 user = self.page.request.user 445 return user and user.may.delete(self.page.page_name) 446 447 # Store-specific methods. 448 449 def mtime(self): 450 return self.store.mtime() 451 452 # High-level methods. 453 454 def keys(self): 455 456 "Return a list of keys for items in the store." 457 458 if not self.can_read(): 459 return 0 460 461 return self.store.keys() 462 463 def append(self, item): 464 465 "Append the given 'item' to the store." 466 467 if not self.can_write(): 468 return 469 470 self.store.append(item) 471 472 def __len__(self): 473 474 "Return the number of items in the store." 475 476 if not self.can_read(): 477 return 0 478 479 return len(self.store) 480 481 def __getitem__(self, number): 482 483 "Return the item with the given 'number'." 484 485 if not self.can_read(): 486 raise IndexError, number 487 488 return self.store.__getitem__(number) 489 490 def __delitem__(self, number): 491 492 "Remove the item with the given 'number'." 493 494 if not self.can_delete(): 495 return 496 497 return self.store.__delitem__(number) 498 499 def __iter__(self): 500 return self.store.__iter__() 501 502 def next(self): 503 return self.store.next() 504 505 # Convenience store classes. 506 507 class ItemStore(ItemStoreBase): 508 509 "Store items in a directory via a page." 510 511 def __init__(self, page, item_dir="items", lock_dir="item_locks"): 512 ItemStoreBase.__init__(self, page, getDirectoryItemStoreForPage(page, item_dir, lock_dir)) 513 514 class ItemSubpageStore(ItemStoreBase): 515 516 "Store items in subpages of a page." 517 518 def __init__(self, page, lock_dir="item_locks"): 519 ItemStoreBase.__init__(self, page, getSubpageItemStoreForPage(page, lock_dir)) 520 521 # vim: tabstop=4 expandtab shiftwidth=4