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 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 # Collect the strict subpages of the parent page. 252 253 leafnames = [] 254 parentname = self.page.page_name 255 256 for page in getPagesForSearch("title:regex:^%s/" % parentname, self.page.request): 257 basename, leafname = page.page_name.rsplit("/", 1) 258 259 # Only collect numbered pages immediately below the parent. 260 261 if basename == parentname and leafname.isdigit(): 262 leafnames.append(int(leafname)) 263 264 return leafnames 265 266 def write_item(self, item, next): 267 268 "Write the given 'item' to a file with the given 'next' item number." 269 270 page = PageEditor(self.page.request, self.get_item_path(next)) 271 page.saveText(item, 0) 272 273 def read_item(self, number): 274 275 "Read the item with the given item 'number'." 276 277 page = Page(self.page.request, self.get_item_path(number)) 278 return page.get_raw_body() 279 280 def remove_item(self, number): 281 282 "Remove the item with the given item 'number'." 283 284 page = PageEditor(self.page.request, self.get_item_path(number)) 285 page.deletePage() 286 287 def get_item_path(self, number): 288 289 "Get the path for the given item 'number'." 290 291 return "%s/%s" % (self.page.page_name, number) 292 293 # High-level methods. 294 295 def append(self, item): 296 297 "Append the given 'item' to the store." 298 299 self.writelock.acquire() 300 try: 301 next = self.get_next() 302 self.write_item(item, next) 303 finally: 304 self.writelock.release() 305 306 class ItemIterator: 307 308 "An iterator over items in a store." 309 310 def __init__(self, store, direction=1): 311 self.store = store 312 self.direction = direction 313 self.reset() 314 315 def reset(self): 316 if self.direction == 1: 317 self._next = 0 318 self.final = self.store.next() 319 else: 320 self._next = self.store.next() - 1 321 self.final = 0 322 323 def more(self): 324 if self.direction == 1: 325 return self._next < self.final 326 else: 327 return self._next >= self.final 328 329 def get_next(self): 330 next = self._next 331 self._next += self.direction 332 return next 333 334 def next(self): 335 while self.more(): 336 try: 337 return self.store[self.get_next()] 338 except IndexError: 339 pass 340 341 raise StopIteration 342 343 def reverse(self): 344 self.direction = -self.direction 345 self.reset() 346 347 def reversed(self): 348 self.reverse() 349 return self 350 351 def __iter__(self): 352 return self 353 354 def getDirectoryItemStoreForPage(page, item_dir, lock_dir): 355 356 """ 357 A convenience function returning a directory-based store for the given 358 'page', using the given 'item_dir' and 'lock_dir'. 359 """ 360 361 item_dir_path = tuple(item_dir.split("/")) 362 lock_dir_path = tuple(lock_dir.split("/")) 363 return DirectoryItemStore(page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path)) 364 365 def getSubpageItemStoreForPage(page, lock_dir): 366 367 """ 368 A convenience function returning a subpage-based store for the given 369 'page', using the given 'lock_dir'. 370 """ 371 372 lock_dir_path = tuple(lock_dir.split("/")) 373 return SubpageItemStore(page, page.getPagePath(*lock_dir_path)) 374 375 # Page-oriented item store classes. 376 377 class ItemStoreBase: 378 379 "Access item stores via pages, observing page access restrictions." 380 381 def __init__(self, page, store): 382 self.page = page 383 self.store = store 384 385 def can_write(self): 386 387 """ 388 Return whether the user associated with the request can write to the 389 page owning this store. 390 """ 391 392 user = self.page.request.user 393 return user and user.may.write(self.page.page_name) 394 395 def can_read(self): 396 397 """ 398 Return whether the user associated with the request can read from the 399 page owning this store. 400 """ 401 402 user = self.page.request.user 403 return user and user.may.read(self.page.page_name) 404 405 def can_delete(self): 406 407 """ 408 Return whether the user associated with the request can delete the 409 page owning this store. 410 """ 411 412 user = self.page.request.user 413 return user and user.may.delete(self.page.page_name) 414 415 # Store-specific methods. 416 417 def mtime(self): 418 return self.store.mtime() 419 420 # High-level methods. 421 422 def keys(self): 423 424 "Return a list of keys for items in the store." 425 426 if not self.can_read(): 427 return 0 428 429 return self.store.keys() 430 431 def append(self, item): 432 433 "Append the given 'item' to the store." 434 435 if not self.can_write(): 436 return 437 438 self.store.append(item) 439 440 def __len__(self): 441 442 "Return the number of items in the store." 443 444 if not self.can_read(): 445 return 0 446 447 return len(self.store) 448 449 def __getitem__(self, number): 450 451 "Return the item with the given 'number'." 452 453 if not self.can_read(): 454 raise IndexError, number 455 456 return self.store.__getitem__(number) 457 458 def __delitem__(self, number): 459 460 "Remove the item with the given 'number'." 461 462 if not self.can_delete(): 463 return 464 465 return self.store.__delitem__(number) 466 467 def __iter__(self): 468 return self.store.__iter__() 469 470 def next(self): 471 return self.store.next() 472 473 # Convenience store classes. 474 475 class ItemStore(ItemStoreBase): 476 477 "Store items in a directory via a page." 478 479 def __init__(self, page, item_dir="items", lock_dir="item_locks"): 480 ItemStoreBase.__init__(self, page, getDirectoryItemStoreForPage(page, item_dir, lock_dir)) 481 482 class ItemSubpageStore(ItemStoreBase): 483 484 "Store items in subpages of a page." 485 486 def __init__(self, page, lock_dir="item_locks"): 487 ItemStoreBase.__init__(self, page, getSubpageItemStoreForPage(page, lock_dir)) 488 489 # vim: tabstop=4 expandtab shiftwidth=4