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