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