1 #!/usr/bin/env python 2 3 """ 4 A setup and configuration script for MoinMoin. 5 6 Copyright (C) 2010, 2011, 2012, 2013 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 os.path import abspath, exists, extsep, isdir, islink, join, normpath, split 23 from os import chdir, chmod, listdir, mkdir, makedirs, remove, rename, rmdir 24 from getpass import getpass 25 from glob import glob 26 from zipfile import ZipFile 27 import os 28 import py_compile 29 import re 30 import shutil 31 import sys 32 import tempfile 33 34 __version__ = "0.4" 35 36 # Regular expressions for editing MoinMoin scripts and configuration files. 37 38 def compile_definition(name): 39 return re.compile(r"^(\s*)#*\s*(%s =)\s*(.*)$" % name, re.MULTILINE) 40 41 wikiconfig_import = re.compile(r"^(\s*)#*\s*" 42 r"(from\s+)(?P<module>\S+)" 43 r"(\s+import\s+)(?P<names>(?:\S|[^,\s])+(?:\s*,\s*(?:\S|[^,\s])+)*)" 44 r"(\s*)$", re.MULTILINE) 45 46 moin_cgi_prefix = re.compile(r"^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE) 47 moin_cgi_wikiconfig = re.compile(r"^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE) 48 moin_cgi_properties = compile_definition("properties") 49 moin_cgi_fix_script_name = compile_definition("fix_script_name") 50 moin_cgi_force_cgi = re.compile(r"^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE) 51 52 css_import_stylesheet = re.compile(r"(\s*@import\s+[\"'])(.*?)([\"']\s*;)") 53 54 # Templates for Apache site definitions. 55 56 apache_site = """\ 57 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi" 58 """ 59 60 apache_site_extra = """\ 61 Alias %(static_url_resources_path)s "%(htdocs_dir)s/" 62 """ 63 64 # Limited hosting .htaccess definitions require the following settings to be 65 # configured in the main Apache configuration files for the directory where 66 # .htaccess is to be deployed: 67 # 68 # AllowOverride FileInfo Indexes 69 # 70 # The following settings are required for the directory where the moin.cgi 71 # script is to be deployed: 72 # 73 # Options ExecCGI FollowSymLinks SymLinksIfOwnerMatch 74 # AddHandler cgi-script .cgi 75 # 76 # If a DirectoryIndex directive is also desired, the Indexes option must be set. 77 # Such a directive is not desirable where the static and dynamic resources are 78 # in different places, however. 79 80 apache_htaccess_combined_mod_rewrite = """\ 81 DirectoryIndex %(url_path_tr)s/moin.cgi/ 82 RewriteEngine On 83 RewriteBase %(final_url_path)s 84 RewriteCond %%{REQUEST_FILENAME} !-f 85 RewriteCond %%{REQUEST_FILENAME} !-d 86 RewriteRule ^(.*) %(url_path_tr)s/moin.cgi/$1 [PT,L,QSA] 87 """ 88 89 # Post-setup templates. 90 91 postsetup_setfacl = """\ 92 #!/bin/sh 93 94 find '%(common_dir)s/data' -type f | xargs setfacl -m u:%(web_user)s:rw 95 find '%(common_dir)s/data' -type d | xargs setfacl -m u:%(web_user)s:rwx 96 find '%(common_dir)s/data/plugin' -type f | xargs setfacl -m u:%(web_user)s:r 97 find '%(common_dir)s/data/plugin' -type d | xargs setfacl -m u:%(web_user)s:rx 98 find '%(common_dir)s/underlay' -type f | xargs setfacl -m u:%(web_user)s:rw 99 find '%(common_dir)s/underlay' -type d | xargs setfacl -m u:%(web_user)s:rwx 100 if [ -e "%(common_dir)s/wikiconfig.py" ]; then 101 setfacl -m u:%(web_user)s:r '%(common_dir)s/wikiconfig.py' 102 fi 103 """ 104 105 postsetup_setfacl_extra = """ 106 find '%(htdocs_dir)s' -type f | xargs setfacl -m u:%(web_user)s:r 107 find '%(htdocs_dir)s' -type d | xargs setfacl -m u:%(web_user)s:rx 108 """ 109 110 postsetup_setfacl_logs = """ 111 if [ -e "%(common_dir)s/data/*-log" ]; then 112 setfacl -m u:%(web_user)s:rw %(common_dir)s/data/*-log 113 fi 114 """ 115 116 postsetup_chown_chmod = """\ 117 #!/bin/sh 118 119 chown -R %(this_user)s.%(web_group)s '%(common_dir)s/data' 120 chown -R %(this_user)s.%(web_group)s '%(common_dir)s/underlay' 121 find '%(common_dir)s/data' -type f | xargs chmod g=rw 122 find '%(common_dir)s/data' -type d | xargs chmod g=rwx 123 find '%(common_dir)s/data/plugin' -type f | xargs chmod g=r 124 find '%(common_dir)s/data/plugin' -type d | xargs chmod g=rx 125 find '%(common_dir)s/underlay' -type f | xargs chmod g=rw 126 find '%(common_dir)s/underlay' -type d | xargs chmod g=rwx 127 if [ -e "%(common_dir)s/wikiconfig.py" ]; then 128 chown %(this_user)s.%(web_group)s '%(common_dir)s/wikiconfig.py' 129 chmod g=r '%(common_dir)s/wikiconfig.py' 130 fi 131 """ 132 133 postsetup_chown_extra = """ 134 chown -R %(this_user)s.%(web_group)s '%(htdocs_dir)s' 135 """ 136 137 postsetup_chown_logs = """ 138 if [ -e "%(common_dir)s/data/*-log" ]; then 139 chown %(this_user)s.%(web_group)s %(common_dir)s/data/*-log 140 fi 141 """ 142 143 # Utility functions. 144 145 def readfile(filename): 146 f = open(filename) 147 try: 148 return f.read() 149 finally: 150 f.close() 151 152 def writefile(filename, s): 153 f = open(filename, "w") 154 try: 155 f.write(s) 156 finally: 157 f.close() 158 159 def status(message): 160 print message 161 162 note = status 163 error = status 164 165 def format(s, indent): 166 return re.sub("\n\s+", "\n" + " " * indent, s) 167 168 # Classes. 169 170 class SetupException(Exception): 171 172 "An exception indicating a problem with a setup action." 173 174 pass 175 176 class Configuration: 177 178 "A class representing the configuration." 179 180 special_names = ["site_name"] 181 182 def __init__(self, filename): 183 self.content = readfile(filename) 184 self.filename = filename 185 186 def get_pattern(self, name): 187 188 # Make underscores optional for certain names. 189 190 if name in self.special_names: 191 name = name.replace("_", "_?") 192 193 return compile_definition(name) 194 195 def get(self, name): 196 197 """ 198 Return the raw value of the last definition having the given 'name'. 199 """ 200 201 pattern = self.get_pattern(name) 202 results = [match.group(3) for match in pattern.finditer(self.content)] 203 if results: 204 return results[-1] 205 else: 206 return None 207 208 def set(self, name, value, count=None, raw=0): 209 210 """ 211 Set the configuration parameter having the given 'name' with the given 212 'value', limiting the number of appropriately named parameters changed 213 to 'count', if specified. 214 215 If the configuration parameter of the given 'name' does not exist, 216 insert such a parameter at the end of the file. 217 218 If the optional 'raw' parameter is specified and set to a true value, 219 the provided 'value' is inserted directly into the configuration file. 220 """ 221 222 if not self.replace(name, value, count, raw): 223 self.insert(name, value, raw) 224 225 def replace(self, name, value, count=None, raw=0): 226 227 """ 228 Replace configuration parameters having the given 'name' with the given 229 'value', limiting the number of appropriately named parameters changed 230 to 'count', if specified. 231 232 If the optional 'raw' parameter is specified and set to a true value, 233 the provided 'value' is inserted directly into the configuration file. 234 235 Return the number of substitutions made. 236 """ 237 238 if raw: 239 substitution = r"\1\2 %s" % value 240 else: 241 substitution = r"\1\2 %r" % value 242 243 pattern = self.get_pattern(name) 244 245 if count is None: 246 self.content, n = pattern.subn(substitution, self.content) 247 else: 248 self.content, n = pattern.subn(substitution, self.content, count=count) 249 250 return n 251 252 def insert(self, name, value, raw=0): 253 254 """ 255 Insert the configuration parameter having the given 'name' and 'value'. 256 257 If the optional 'raw' parameter is specified and set to a true value, 258 the provided 'value' is inserted directly into the configuration file. 259 """ 260 261 if raw: 262 insertion = "%s = %s" 263 else: 264 insertion = "%s = %r" 265 266 self.insert_text(insertion % (name, value)) 267 268 def insert_text(self, text): 269 270 "Insert the given 'text' at the end of the configuration." 271 272 if not self.content.endswith("\n"): 273 self.content += "\n" 274 self.content += " %s\n" % text 275 276 def set_import(self, imported_module, imported_names): 277 278 """ 279 Set up an import of the given 'imported_module' exposing the given 280 'imported_names'. 281 """ 282 283 s = self.content 284 after_point = 0 285 first_point = None 286 287 for module_import in wikiconfig_import.finditer(s): 288 before, from_keyword, module, import_keyword, names, after = module_import.groups() 289 before_point, after_point = module_import.span() 290 291 if first_point is None: 292 first_point = after_point 293 294 names = [name.strip() for name in names.split(",")] 295 296 # Test the import for a reference to the requested imported module. 297 298 if imported_module == module: 299 for name in imported_names: 300 if name not in names: 301 names.append(name) 302 303 self.content = s[:before_point] + ( 304 "%s%s%s%s%s%s" % (before, from_keyword, module, import_keyword, ", ".join(names), after) 305 ) + s[after_point:] 306 break 307 308 # Where no import references the imported module, insert a reference 309 # into the configuration. 310 311 else: 312 # Add the import after the first one. 313 314 if first_point is not None: 315 self.content = s[:first_point] + ("\nfrom %s import %s" % (imported_module, ", ".join(imported_names))) + s[first_point:] 316 317 def close(self): 318 319 "Close the file, writing the content." 320 321 writefile(self.filename, self.content) 322 323 class Installation: 324 325 "A class for installing and initialising MoinMoin." 326 327 method_names = ( 328 "show_config", 329 "setup", 330 "setup_wiki", 331 "install_moin", 332 "install_data", 333 "install_static_data", 334 "configure_moin", 335 "edit_moin_script", 336 "edit_moin_web_script", 337 "add_superuser", 338 "make_site_files", 339 "make_post_install_script", 340 341 # Post-installation activities. 342 343 "reconfigure_moin", 344 "set_auth_method", 345 "migrate_instance", 346 "install_theme", 347 "install_extension_package", 348 "install_plugins", 349 "install_actions", 350 "install_macros", 351 "install_parsers", 352 "install_event_handlers", 353 "install_theme_resources", 354 "edit_theme_stylesheet", 355 356 # Other activities. 357 358 "make_page_package", 359 "install_page_package", 360 ) 361 362 source_config_names = ( 363 "moin_distribution", "prefix", "site_packages", "prefix_site_packages", 364 "htdocs_dir_source" 365 ) 366 367 instance_config_names = ( 368 "common_dir", "farm_config", "site_config", 369 "site_name", "site_identifier", 370 "front_page_name", "superuser", "theme_default", "htdocs_dir" 371 ) 372 373 site_config_names = ( 374 "web_app_dir", "web_site_dir", "web_static_dir", 375 "url_path", "static_url_path" 376 ) 377 378 # NOTE: Potentially detect Web server credentials. 379 380 web_user_default = "www-data" 381 web_group_default = "www-data" 382 383 # MoinMoin resources. 384 385 theme_master = "modernized" 386 extra_theme_css_files = ["SlideShow.css"] 387 388 def __init__(self, 389 moin_distribution=None, 390 prefix=None, site_packages=None, 391 web_app_dir=None, web_static_dir=None, web_site_dir=None, 392 web_user=None, web_group=None, 393 common_dir=None, farm_config=None, site_config=None, 394 url_path=None, static_url_path=None, 395 superuser=None, site_name=None, site_identifier=None, front_page_name=None, 396 theme_default=None): 397 398 """ 399 Initialise a Wiki installation using the following installation 400 settings: 401 402 * moin_distribution - the directory containing a MoinMoin source 403 distribution (can be omitted) 404 * prefix - the installation prefix (equivalent to /usr) 405 * site_packages - optional: the location of the Python 406 site-packages directory if outside the 'prefix' 407 (overrides the path calculated using 'prefix') 408 * web_app_dir - the directory where Web applications and scripts 409 reside (such as /home/www-user/cgi-bin) 410 * web_static_dir - optional: the directory where static Web 411 resources reside (such as /home/www-user/htdocs) 412 * web_site_dir - optional: the directory where Web site 413 definitions reside (such as 414 /etc/apache2/sites-available) 415 * web_user - optional: the user operating the Web server 416 * web_group - optional: the group of the Web server user 417 418 The following site settings are also applicable: 419 420 * common_dir - the directory where the Wiki configuration, 421 resources and instance will reside (such as 422 /home/www-user/mywiki) 423 * farm_config - optional: any Wiki farm configuration file for 424 multiple Wiki deployments (overrides the 425 'common_dir' setting) 426 * site_config - optional: a specific configuration file location 427 (overrides the 'common_dir' setting) 428 * url_path - the URL path at which the Wiki will be made 429 available (such as / or /mywiki) 430 * static_url_path - optional: the URL path at which static resources 431 will be made available (such as / or /mywiki) 432 * superuser - the name of the site's superuser (such as 433 "AdminUser", can be omitted) 434 * site_name - the name of the site (such as "My Wiki") 435 * site_identifier - optional: an identifier used to refer to the 436 site, typically derived from 'site_name' 437 * front_page_name - the front page name for the site (such as 438 "FrontPage" or a specific name for the site) 439 * theme_default - optional: the default theme (such as modern) 440 """ 441 442 self.moin_distribution = moin_distribution 443 self.superuser = superuser 444 self.site_name = site_name 445 self.site_identifier = site_identifier or site_name.replace(" ", "").lower() 446 self.front_page_name = front_page_name 447 self.farm_config = farm_config 448 self.site_config = site_config 449 self.theme_default = theme_default 450 451 self.web_user = web_user or self.web_user_default 452 self.web_group = web_group or self.web_group_default 453 454 # NOTE: Support the detection of the Apache sites directory. 455 456 self.prefix, self.site_packages, self.web_app_dir, self.web_site_dir, self.web_static_dir, self.common_dir = \ 457 map(self._get_abspath, (prefix, site_packages, web_app_dir, web_site_dir, web_static_dir, common_dir)) 458 459 if not self.web_app_dir: 460 raise TypeError, "The 'web_app_dir' setting must be specified." 461 462 # Strip any trailing "/" from the URL path. 463 464 if not url_path: 465 raise TypeError, "The 'url_path' setting must be specified." 466 467 self.url_path = self._tidy_url_path(url_path) 468 self.static_url_path = static_url_path and self._tidy_url_path(static_url_path) 469 470 self.url_path_tr = self._truncate_url_path(url_path) 471 472 # Define and create specific directories. 473 # Here are the configuration and actual Wiki data directories. 474 475 if not self.common_dir: 476 raise TypeError, "The 'common_dir' setting must be specified." 477 478 # Define the place where the MoinMoin package will actually reside. 479 480 if not self.prefix and not self.site_packages: 481 raise TypeError, "Either the 'prefix' or the 'site_packages' setting must be specified." 482 483 self.prefix_site_packages = self.site_packages or \ 484 join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages") 485 486 # Find the version. 487 488 self.moin_version = self.get_moin_version() 489 490 # The static resources reside in different locations depending on the 491 # version of MoinMoin, but the Web server is used to serve static 492 # resources in both cases, even though MoinMoin 1.9 can serve static 493 # files itself. 494 495 # A shared data directory may be in use. 496 497 self.htdocs_dir_source = join(self.get_moin_data(), "htdocs") 498 499 if self.htdocs_dir_source is None or not exists(self.htdocs_dir_source): 500 501 # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs 502 503 if self.moin_version.startswith("1.9"): 504 self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs") 505 else: 506 raise SetupException, "The static resources could not be found." 507 508 # Add the static identifier to the URL path. For example: 509 # 510 # / -> /moin_static187 511 # /hgwiki -> /hgwiki-moin_static187 512 # 513 # This allows multiple Wiki instances to have their own static resources 514 # in the same hosting area. 515 # 516 # Where a separate static URL path has been given, the resources are 517 # located under that path: 518 # 519 # / -> /moin_static187 520 # /hgwiki -> /hgwiki/moin_static187 521 522 # The final URL path is the principal location of the Wiki. 523 524 self.final_url_path = self.static_url_path or self.url_path 525 526 # The static URL resources path is the specific location of static 527 # resources. 528 529 self.static_url_resources_path = \ 530 self.final_url_path + \ 531 (self.static_url_path 532 and (self.static_url_path != "/" 533 and "/" 534 or "") 535 or (self.final_url_path != "/" 536 and "-" 537 or "")) + \ 538 self.get_static_identifier() 539 540 self.static_dir_name = self.static_url_resources_path.split("/")[-1] 541 542 # In limited hosting, the static resources directory is related to 543 # the URL path, either a specified static URL path or the general path. 544 545 if self.limited_hosting(): 546 self.htdocs_dir = join(self.web_static_dir or self.web_app_dir, self.static_dir_name) 547 548 # Otherwise, a mapping is made to the directory. 549 # This may be placed in a special static directory if desired. 550 551 else: 552 self.htdocs_dir = join(self.web_static_dir or self.common_dir, "htdocs") 553 554 def show_config(self): 555 556 "Show the configuration." 557 558 print 559 if self.limited_hosting(): 560 print "Limited hosting configuration detected." 561 print "Published resources will be configured using .htaccess." 562 else: 563 print "Privileged hosting configuration detected..." 564 print "Published resources will be configured using site definition files." 565 print 566 for section in ("source", "instance", "site"): 567 print section.title() 568 print "-" * len(section) 569 print 570 for setting in getattr(self, "%s_config_names" % section): 571 print "%-24s%s" % (setting, getattr(self, setting)) 572 print 573 574 print "Configuration locations" 575 print "-----------------------" 576 print 577 print "%-24s%s" % ("site-level", self.get_site_config()) 578 print "%-24s%s" % ("global", self.get_global_config()) 579 print 580 print "Theme directories" 581 print "-----------------" 582 print 583 584 try: 585 for theme in self.get_theme_directories(): 586 print "%-24s%s" % theme 587 except OSError, exc: 588 print "Not shown:", str(exc) 589 590 def _get_abspath(self, d): 591 return d and abspath(d) or None 592 593 def _tidy_url_path(self, url_path): 594 if url_path != "/" and url_path.endswith("/"): 595 return url_path[:-1] 596 else: 597 return url_path 598 599 def _truncate_url_path(self, url_path): 600 if url_path.endswith("/"): 601 return url_path[:-1] 602 else: 603 return url_path 604 605 def _set_pythonpath(self): 606 path = os.environ.get("PYTHONPATH", "") 607 608 conf_dir = self.get_wikiconfig_directory() 609 610 if path: 611 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + conf_dir 612 else: 613 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + conf_dir 614 615 return path 616 617 def _reset_pythonpath(self, path): 618 if path: 619 os.environ["PYTHONPATH"] = path 620 else: 621 del os.environ["PYTHONPATH"] 622 623 def get_moin_version(self): 624 625 "Return the MoinMoin version." 626 627 this_dir = os.getcwd() 628 629 for dir in [self.moin_distribution, self.prefix_site_packages]: 630 if dir: 631 try: 632 chdir(dir) 633 version = self.get_moin_version_from_package_info() or \ 634 self.get_moin_version_from_import() 635 if version: 636 return version 637 638 finally: 639 chdir(this_dir) 640 else: 641 return self.get_moin_version_from_import() 642 643 def get_moin_version_from_package_info(self): 644 645 "Inspect the MoinMoin package information, returning the version." 646 647 try: 648 f = open("PKG-INFO") 649 try: 650 for line in f.xreadlines(): 651 columns = line.split() 652 if columns[0] == "Version:": 653 return columns[1] 654 finally: 655 f.close() 656 657 except IOError: 658 pass 659 660 return None 661 662 def get_moin_version_from_import(self): 663 664 "Return the MoinMoin version from an import of the package itself." 665 666 # Where no distribution information can be read, try and import an 667 # installed version module. 668 669 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable) 670 try: 671 return f.read().strip() 672 finally: 673 f.close() 674 675 def get_moin_data(self): 676 677 "Return the exact location of MoinMoin data." 678 679 return self.moin_distribution and join(self.moin_distribution, "wiki") or \ 680 self.prefix and join(self.prefix, "share", "moin") or None 681 682 def get_moin_script(self): 683 684 "Return the location of the general-purpose moin script." 685 686 return join(self.prefix, "bin", "moin") 687 688 def get_wikiconfig_directory(self): 689 690 "Return the location of the Wiki configuration." 691 692 if self.site_config: 693 return split(self.site_config)[0] 694 else: 695 return self.common_dir 696 697 def get_site_config(self): 698 699 "Return the file providing the site-level configuration." 700 701 if self.site_config: 702 return self.site_config 703 else: 704 return join(self.common_dir, "wikiconfig.py") 705 706 def get_global_config(self): 707 708 "Return the file providing the global MoinMoin configuration." 709 710 if self.farm_config: 711 return self.farm_config 712 else: 713 return join(self.common_dir, "wikiconfig.py") 714 715 def get_static_identifier(self): 716 717 "Return the static URL/directory identifier for the Wiki." 718 719 return "moin_static%s" % self.moin_version.replace(".", "") 720 721 def get_plugin_directory(self, plugin_type): 722 723 "Return the directory for plugins of the given 'plugin_type'." 724 725 data_dir = join(self.common_dir, "data") 726 return join(data_dir, "plugin", plugin_type) 727 728 def limited_hosting(self): 729 730 "Return whether limited Web hosting is being used." 731 732 return not self.web_site_dir 733 734 def ensure_directories(self): 735 736 "Make sure that all the directories are available." 737 738 for d in (self.common_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir): 739 if d is not None and not exists(d): 740 makedirs(d) 741 742 def get_theme_directories(self, theme_name=None): 743 744 """ 745 Return tuples of the form (theme name, theme directory) for all themes, 746 or for a single theme if the optional 'theme_name' is specified. 747 """ 748 749 filenames = theme_name and [theme_name] or listdir(self.htdocs_dir) 750 directories = [] 751 752 for filename in filenames: 753 theme_dir = join(self.htdocs_dir, filename) 754 755 if not exists(theme_dir) or not isdir(theme_dir): 756 continue 757 758 directories.append((filename, theme_dir)) 759 760 return directories 761 762 # Main methods. 763 764 def setup(self): 765 766 "Set up the installation." 767 768 self.ensure_directories() 769 self.install_moin() 770 self.edit_moin_script() 771 self._setup_wiki() 772 773 def setup_wiki(self): 774 775 "Set up a Wiki without installing MoinMoin." 776 777 self.ensure_directories() 778 self._setup_wiki() 779 780 def _setup_wiki(self): 781 782 "Set up a Wiki without installing MoinMoin." 783 784 self.install_data() 785 self.install_static_data() 786 self.configure_moin() 787 self.add_superuser() 788 self.edit_moin_web_script(self.make_site_files()) 789 self.make_post_install_script() 790 791 if self.moin_version.startswith("1.9"): 792 note("You may need to visit the LanguageSetup page in the Wiki to create the standard set of pages.") 793 794 def install_moin(self): 795 796 "Enter the distribution directory and run the setup script." 797 798 # NOTE: Possibly check for an existing installation and skip repeated 799 # NOTE: installation attempts. 800 801 if not self.moin_distribution: 802 raise SetupException, "Cannot install MoinMoin without a 'moin_distribution' setting being defined." 803 804 this_dir = os.getcwd() 805 chdir(self.moin_distribution) 806 807 log_filename = "install-%s.log" % split(self.common_dir)[-1] 808 809 status("Installing MoinMoin in %s..." % self.prefix) 810 811 install_cmd = "install" 812 options = "--prefix='%s' --record='%s'" % (self.prefix, log_filename) 813 814 os.system("%s setup.py --quiet %s %s --force" % (sys.executable, install_cmd, options)) 815 816 chdir(this_dir) 817 818 def install_data(self): 819 820 "Install Wiki data into an instance." 821 822 moin_data = self.get_moin_data() 823 824 if not moin_data: 825 raise SetupException, \ 826 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'prefix' setting being defined." 827 828 # The default wikiconfig assumes data and underlay in the same directory. 829 830 status("Installing data and underlay in %s..." % self.common_dir) 831 832 for d in ("data", "underlay"): 833 source = join(moin_data, d) 834 source_tar = source + extsep + "tar" 835 836 if exists(source): 837 shutil.copytree(source, join(self.common_dir, d)) 838 elif exists(source_tar): 839 840 note("Copying archive %s instead of directory %s. Running...\n" 841 "make pagepacks\n" 842 "in the distribution directory should rectify this." % (source_tar, source)) 843 844 shutil.copy(source_tar, self.common_dir) 845 os.system("tar xf %s -C %s" % (source_tar, self.common_dir)) 846 else: 847 status("Could not copy %s into installed Wiki." % d) 848 849 def install_static_data(self): 850 851 "Install static Web data if appropriate." 852 853 if not exists(self.htdocs_dir): 854 mkdir(self.htdocs_dir) 855 856 for item in listdir(self.htdocs_dir_source): 857 path = join(self.htdocs_dir_source, item) 858 if isdir(path): 859 shutil.copytree(path, join(self.htdocs_dir, item)) 860 else: 861 shutil.copy(path, join(self.htdocs_dir, item)) 862 863 def configure_moin(self, reset=0): 864 865 """ 866 Edit the Wiki configuration file. If the optional 'reset' parameter is 867 specified as a true value, a default configuration will be copied from 868 the distribution if appropriate. 869 """ 870 871 moin_data = self.get_moin_data() 872 873 if not moin_data: 874 raise SetupException, \ 875 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'prefix' setting being defined." 876 877 # NOTE: MoinMoin usually uses an apparently common URL space associated 878 # NOTE: with the version, but more specific locations are probably 879 # NOTE: acceptable if less efficient. 880 881 url_prefix_static = "%r" % self.static_url_resources_path 882 883 # Use a farm configuration file. 884 885 if self.farm_config: 886 wikiconfig_py = self.farm_config 887 888 # Or copy the Wiki configuration file from the distribution. 889 890 else: 891 wikiconfig_py = join(self.common_dir, "wikiconfig.py") 892 893 if not exists(wikiconfig_py) or reset: 894 shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py) 895 896 status("Editing configuration from %s..." % wikiconfig_py) 897 898 # Edit the Wiki configuration file. 899 900 wikiconfig = Configuration(wikiconfig_py) 901 902 try: 903 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 904 if self.superuser: 905 wikiconfig.set("superuser", [self.superuser]) 906 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 907 else: 908 note("Superuser not defined. The ACL rules should be fixed in the configuration.") 909 910 # Edit any created Wiki configuration. 911 912 if not self.site_config: 913 self._configure_moin(wikiconfig) 914 915 finally: 916 wikiconfig.close() 917 918 # Edit any separate site configuration file. 919 920 if self.site_config: 921 status("Editing configuration from %s..." % self.site_config) 922 923 wikiconfig = Configuration(self.site_config) 924 925 try: 926 self._configure_moin(wikiconfig) 927 finally: 928 wikiconfig.close() 929 930 def _configure_moin(self, wikiconfig): 931 932 """ 933 Configure Moin, accessing the configuration file using 'wikiconfig'. 934 """ 935 936 # Specific site configurations also appear to need 'data_dir', even in 937 # 1.9. 938 939 if not self.moin_version.startswith("1.9") or self.site_config: 940 data_dir = join(self.common_dir, "data") 941 data_underlay_dir = join(self.common_dir, "underlay") 942 943 wikiconfig.set("data_dir", data_dir) 944 wikiconfig.set("data_underlay_dir", data_underlay_dir) 945 946 wikiconfig.set("site_name", self.site_name) 947 wikiconfig.set("page_front_page", self.front_page_name, count=1) 948 949 if self.theme_default is not None: 950 wikiconfig.set("theme_default", self.theme_default) 951 952 def edit_moin_script(self): 953 954 "Edit the moin script." 955 956 moin_script = self.get_moin_script() 957 status("Editing moin script at %s..." % moin_script) 958 959 s = readfile(moin_script) 960 s = s.replace("#!/usr/bin/env python", "#!%s" % sys.executable) 961 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 962 963 writefile(moin_script, s) 964 965 def edit_moin_web_script(self, site_file_configured=1): 966 967 "Edit and install CGI script." 968 969 # NOTE: CGI only so far. 970 # NOTE: Permissions should be checked. 971 972 moin_data = self.get_moin_data() 973 974 if self.moin_version.startswith("1.9"): 975 moin_cgi_script = "moin.fcgi" 976 else: 977 moin_cgi_script = "moin.cgi" 978 979 moin_cgi = join(moin_data, "server", moin_cgi_script) 980 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 981 982 status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed)) 983 984 s = readfile(moin_cgi) 985 s = s.replace("#!/usr/bin/env python", "#!%s" % sys.executable) 986 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 987 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.common_dir, s) 988 989 # Handle differences in script names when using limited hosting with 990 # URL rewriting. 991 992 if self.limited_hosting(): 993 if not site_file_configured: 994 note("Site file not configured: script name not changed.") 995 else: 996 url_path = self.final_url_path 997 998 if self.moin_version.startswith("1.9"): 999 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % url_path, s) 1000 else: 1001 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : url_path}, s) 1002 1003 # NOTE: Use CGI for now. 1004 1005 if self.moin_version.startswith("1.9"): 1006 s = moin_cgi_force_cgi.sub(r"\1", s) 1007 1008 writefile(moin_cgi_installed, s) 1009 os.system("chmod a+rx '%s'" % moin_cgi_installed) 1010 1011 # Fix the cause of opaque errors in some Apache environments. 1012 1013 os.system("chmod go-w '%s'" % moin_cgi_installed) 1014 1015 def add_superuser(self): 1016 1017 "Add the superuser account." 1018 1019 if not self.superuser: 1020 return 1021 1022 print "Creating superuser", self.superuser, "using..." 1023 email = raw_input("E-mail address: ") 1024 password = getpass("Password: ") 1025 1026 path = self._set_pythonpath() 1027 1028 cmd = "%s --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % ( 1029 self.get_moin_script(), self.common_dir, self.superuser, email, password) 1030 os.system(cmd) 1031 1032 self._reset_pythonpath(path) 1033 1034 def make_site_files(self): 1035 1036 "Make the Apache site files." 1037 1038 # NOTE: Using local namespace for substitution. 1039 1040 # Where the site definitions and applications directories are different, 1041 # use a normal site definition. 1042 1043 if not self.limited_hosting(): 1044 1045 site_def = join(self.web_site_dir, self.site_identifier) 1046 1047 s = apache_site % self.__dict__ 1048 s += apache_site_extra % self.__dict__ 1049 1050 status("Writing Apache site definitions to %s..." % site_def) 1051 writefile(site_def, s) 1052 1053 note("Copy the site definitions to the appropriate sites directory if appropriate.\n" 1054 "Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).") 1055 1056 return 1 1057 1058 # Otherwise, use an .htaccess file. 1059 1060 else: 1061 site_def = join(self.web_static_dir or self.web_app_dir, ".htaccess") 1062 1063 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 1064 1065 status("Writing .htaccess file to %s..." % site_def) 1066 try: 1067 writefile(site_def, s) 1068 except IOError: 1069 note("The .htaccess file could not be written. This will also affect the script name setting.") 1070 return 0 1071 else: 1072 return 1 1073 1074 def make_post_install_script(self): 1075 1076 "Write a post-install script with additional actions." 1077 1078 # Work out whether setfacl works. 1079 1080 fd, temp_filename = tempfile.mkstemp(dir=self.common_dir) 1081 os.close(fd) 1082 1083 have_setfacl = os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % { 1084 "web_user" : self.web_user, "file" : temp_filename}) == 0 1085 1086 remove(temp_filename) 1087 1088 # Create the scripts. 1089 1090 this_user = os.environ["USER"] 1091 postinst_scripts = "moinsetup-post-chown.sh", "moinsetup-post-setfacl.sh" 1092 1093 vars = {} 1094 vars.update(Installation.__dict__) 1095 vars.update(self.__dict__) 1096 vars.update(locals()) 1097 1098 for postinst_script, start, extra, logs in [ 1099 (postinst_scripts[0], postsetup_chown_chmod, postsetup_chown_extra, postsetup_chown_logs), 1100 (postinst_scripts[1], postsetup_setfacl, postsetup_setfacl_extra, postsetup_setfacl_logs) 1101 ]: 1102 1103 s = start % vars 1104 s += extra % vars 1105 s += logs % vars 1106 1107 writefile(postinst_script, s) 1108 chmod(postinst_script, 0755) 1109 1110 if have_setfacl: 1111 note("Run %s to set file ownership and permissions.\n" 1112 "If this somehow fails..." % postinst_scripts[1]) 1113 1114 note("Run %s as root to set file ownership and permissions." % postinst_scripts[0]) 1115 1116 # Accessory methods. 1117 1118 def reconfigure_moin(self, name=None, value=None, raw=0): 1119 1120 """ 1121 Edit the installed Wiki configuration file, setting a parameter with any 1122 given 'name' to the given 'value', treating the value as a raw 1123 expression (not a string) if 'raw' is set to a true value. 1124 1125 If 'name' and the remaining parameters are omitted, the default 1126 configuration activity is performed. 1127 1128 If the 'site_config' setting is defined, the specific site configuration 1129 will be changed. 1130 """ 1131 1132 wikiconfig_py = self.get_site_config() 1133 1134 status("Editing configuration from %s..." % wikiconfig_py) 1135 1136 wikiconfig = Configuration(wikiconfig_py) 1137 1138 try: 1139 # Perform default configuration. 1140 1141 if name is None and value is None: 1142 self._configure_moin(wikiconfig) 1143 else: 1144 wikiconfig.set(name, value, raw=raw) 1145 1146 finally: 1147 wikiconfig.close() 1148 1149 def set_auth_method(self, method_name): 1150 1151 """ 1152 Edit the installed Wiki configuration file, configuring the 1153 authentication method having the given 'method_name'. 1154 1155 Currently recognised authentication methods are: 1156 1157 * openid (uses OpenIDAuth to access OpenID providers) 1158 * moin, default (use MoinAuth to provide a login form) 1159 * given, remote-user (use HTTPAuth to obtain Web server credentials) 1160 1161 If the 'farm_config' setting is defined, the Wiki farm configuration 1162 will be changed. 1163 """ 1164 1165 wikiconfig_py = self.get_global_config() 1166 1167 status("Editing configuration from %s..." % wikiconfig_py) 1168 1169 wikiconfig = Configuration(wikiconfig_py) 1170 1171 try: 1172 # OpenID authentication. 1173 1174 if method_name.lower() == "openid": 1175 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"]) 1176 1177 if self.moin_version.startswith("1.9"): 1178 if wikiconfig.get("cookie_lifetime"): 1179 wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1) 1180 else: 1181 wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1) 1182 else: 1183 if wikiconfig.get("anonymous_session_lifetime"): 1184 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 1185 else: 1186 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 1187 1188 auth_object = "OpenIDAuth()" 1189 1190 # Default Moin authentication. 1191 1192 elif method_name.lower() in ("moin", "default"): 1193 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"]) 1194 auth_object = "MoinAuth()" 1195 1196 # REMOTE_USER authentication. 1197 1198 elif method_name.lower() in ("given", "remote-user"): 1199 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"]) 1200 auth_object = "HTTPAuth(autocreate=True)" 1201 1202 # Other methods are not currently supported. 1203 1204 else: 1205 return 1206 1207 # Edit the authentication setting. 1208 1209 auth = wikiconfig.get("auth") 1210 if auth: 1211 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1) 1212 else: 1213 wikiconfig.set("auth", "[%s]" % auth_object, raw=1) 1214 1215 finally: 1216 wikiconfig.close() 1217 1218 def migrate_instance(self, test=0, change_site=0): 1219 1220 """ 1221 Migrate the Wiki to the currently supported layout. If 'test' is 1222 specified and set to a non-empty or true value, only print whether the 1223 migration can be performed. 1224 1225 If 'change_site' is specified and set to a non-empty or true value, the 1226 site definitions will be updated; this will overwrite any changes made 1227 to the site definitions after they were last produced by moinsetup, and 1228 care must be taken to ensure that things like access controls are 1229 re-added to the definitions after this action is performed. 1230 """ 1231 1232 conf_dir = join(self.common_dir, "conf") 1233 if exists(conf_dir): 1234 for filename in listdir(conf_dir): 1235 pathname = join(conf_dir, filename) 1236 target = join(self.common_dir, filename) 1237 if not exists(target): 1238 print "Move", filename, "from conf directory." 1239 if not test: 1240 rename(pathname, target) 1241 else: 1242 print "No conf directory." 1243 1244 wikidata = join(self.common_dir, "wikidata") 1245 if exists(wikidata): 1246 htdocs = join(wikidata, "share", "moin", "htdocs") 1247 if exists(htdocs): 1248 target = join(self.common_dir, "htdocs") 1249 if not exists(target): 1250 print "Move htdocs from wikidata directory." 1251 if not test: 1252 rename(htdocs, target) 1253 else: 1254 print "No wikidata directory." 1255 1256 # Remove links and directories. 1257 1258 for name in ("conf", "wikidata"): 1259 d = join(self.common_dir, name) 1260 if islink(d): 1261 print "Remove %s symbolic link." % name 1262 if not test: 1263 remove(d) 1264 1265 if isdir(conf_dir): 1266 print "Remove conf directory." 1267 if not test: 1268 rmdir(conf_dir) 1269 1270 # Add any missing htdocs directory. 1271 1272 if not exists(self.htdocs_dir): 1273 print "Copy htdocs into the instance." 1274 if not test: 1275 self.install_static_data() 1276 1277 # Now attempt to reconfigure the Wiki. 1278 1279 print "Reconfigure the Wiki, the Web script%s." % (change_site and " and the site files" or "") 1280 if not test: 1281 self.configure_moin() 1282 self.edit_moin_web_script() 1283 if change_site: 1284 self.make_site_files() 1285 1286 def install_theme(self, theme_dir, theme_name=None): 1287 1288 """ 1289 Install Wiki theme provided in the given 'theme_dir' having the given 1290 optional 'theme_name' (if different from the 'theme_dir' name). 1291 """ 1292 1293 theme_dir = normpath(theme_dir) 1294 theme_name = theme_name or split(theme_dir)[-1] 1295 theme_module = join(theme_dir, theme_name + extsep + "py") 1296 1297 plugin_theme_dir = self.get_plugin_directory("theme") 1298 1299 # Copy the theme module. 1300 1301 status("Copying theme module to %s..." % plugin_theme_dir) 1302 1303 shutil.copy(theme_module, plugin_theme_dir) 1304 1305 # Copy the resources. 1306 1307 resources_dir = join(self.htdocs_dir, theme_name) 1308 1309 if not exists(resources_dir): 1310 mkdir(resources_dir) 1311 1312 status("Copying theme resources to %s..." % resources_dir) 1313 1314 for d in ("css", "img"): 1315 target_dir = join(resources_dir, d) 1316 if exists(target_dir): 1317 status("Replacing %s..." % target_dir) 1318 shutil.rmtree(target_dir) 1319 shutil.copytree(join(theme_dir, d), target_dir) 1320 1321 # Copy additional resources from other themes. 1322 1323 resources_source_dir = join(self.htdocs_dir, self.theme_master) 1324 target_dir = join(resources_dir, "css") 1325 1326 status("Copying resources from %s..." % resources_source_dir) 1327 1328 for css_file in self.extra_theme_css_files: 1329 css_file_path = join(resources_source_dir, "css", css_file) 1330 if exists(css_file_path): 1331 shutil.copy(css_file_path, target_dir) 1332 1333 note("Don't forget to add theme resources for extensions for this theme.\n" 1334 "Don't forget to edit this theme's stylesheets for extensions.") 1335 1336 def install_extension_package(self, extension_dir): 1337 1338 "Install any libraries from 'extension_dir' using a setup script." 1339 1340 this_dir = os.getcwd() 1341 chdir(extension_dir) 1342 1343 try: 1344 options = "install --install-lib=%s" % self.prefix_site_packages 1345 os.system("%s setup.py %s" % (sys.executable, options)) 1346 finally: 1347 chdir(this_dir) 1348 1349 def install_plugins(self, plugins_dir, plugin_type): 1350 1351 """ 1352 Install Wiki actions provided in the given 'plugins_dir' of the 1353 specified 'plugin_type'. 1354 """ 1355 1356 plugin_target_dir = self.get_plugin_directory(plugin_type) 1357 1358 # Copy the modules. 1359 1360 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 1361 1362 # If a setup file is detected, abandon the installation. 1363 1364 if exists(join(plugins_dir, "setup%spy" % extsep)): 1365 print "Plugins not installed: setup%spy was detected." % extsep 1366 return 1367 1368 for module in glob(join(plugins_dir, "*%spy" % extsep)): 1369 shutil.copy(module, plugin_target_dir) 1370 1371 # Compile the plugin. 1372 1373 plugin = join(plugins_dir, split(module)[-1]) 1374 py_compile.compile(plugin) 1375 1376 def install_actions(self, actions_dir): 1377 1378 "Install Wiki actions provided in the given 'actions_dir'." 1379 1380 self.install_plugins(actions_dir, "action") 1381 1382 def install_macros(self, macros_dir): 1383 1384 "Install Wiki macros provided in the given 'macros_dir'." 1385 1386 self.install_plugins(macros_dir, "macro") 1387 1388 def install_parsers(self, parsers_dir): 1389 1390 "Install Wiki parsers provided in the given 'parsers_dir'." 1391 1392 self.install_plugins(parsers_dir, "parser") 1393 1394 def install_event_handlers(self, events_dir): 1395 1396 "Install Wiki event handlers provided in the given 'events_dir'." 1397 1398 self.install_plugins(events_dir, "events") 1399 1400 def install_theme_resources(self, theme_resources_dir, theme_name=None): 1401 1402 """ 1403 Install theme resources provided in the given 'theme_resources_dir'. If 1404 a specific 'theme_name' is given, only that theme will be given the 1405 specified resources. 1406 """ 1407 1408 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1409 1410 # Copy the resources. 1411 1412 copied = 0 1413 1414 for d in ("css", "img"): 1415 source_dir = join(theme_resources_dir, d) 1416 target_dir = join(theme_dir, d) 1417 1418 if not exists(target_dir): 1419 continue 1420 1421 for resource in glob(join(source_dir, "*%s*" % extsep)): 1422 shutil.copy(resource, target_dir) 1423 copied = 1 1424 1425 if copied: 1426 status("Copied theme resources into %s..." % theme_dir) 1427 1428 note("Don't forget to edit theme stylesheets for any extensions.") 1429 1430 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 1431 1432 """ 1433 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 1434 the 'imported_stylesheet' according to the given 'action' (optional, 1435 defaulting to "ensure"). If a specific 'theme_name' is given, only that 1436 theme will be affected. 1437 """ 1438 1439 if action == "ensure": 1440 ensure = 1 1441 elif action == "remove": 1442 ensure = 0 1443 else: 1444 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 1445 return 1446 1447 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1448 1449 # Locate the resources. 1450 1451 css_dir = join(theme_dir, "css") 1452 1453 if not exists(css_dir): 1454 continue 1455 1456 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 1457 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 1458 1459 if not exists(theme_stylesheet_filename): 1460 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 1461 continue 1462 1463 if not exists(imported_stylesheet_filename): 1464 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 1465 continue 1466 1467 # Edit the resources. 1468 1469 s = readfile(theme_stylesheet_filename) 1470 after_point = 0 1471 1472 for stylesheet_import in css_import_stylesheet.finditer(s): 1473 before, filename, after = stylesheet_import.groups() 1474 before_point, after_point = stylesheet_import.span() 1475 1476 # Test the import for a reference to the requested imported 1477 # stylesheet. 1478 1479 if filename == imported_stylesheet: 1480 if ensure: 1481 break 1482 else: 1483 if s[after_point:after_point+1] == "\n": 1484 after_point += 1 1485 s = "%s%s" % (s[:before_point], s[after_point:]) 1486 1487 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1488 writefile(theme_stylesheet_filename, s) 1489 break 1490 1491 # Where no import references the imported stylesheet, insert a 1492 # reference into the theme stylesheet. 1493 1494 else: 1495 if ensure: 1496 1497 # Assume that the stylesheet can follow other imports. 1498 1499 if s[after_point:after_point+1] == "\n": 1500 after_point += 1 1501 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 1502 1503 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1504 writefile(theme_stylesheet_filename, s) 1505 1506 def make_page_package(self, page_directory, package_filename): 1507 1508 """ 1509 Make a package containing the pages in 'page_directory', using the 1510 filenames as the page names, and writing the package to a file with the 1511 given 'package_filename'. 1512 """ 1513 1514 package = ZipFile(package_filename, "w") 1515 1516 try: 1517 script = ["MoinMoinPackage|1"] 1518 self._add_page_package_files(page_directory, "", package, script) 1519 package.writestr("MOIN_PACKAGE", "\n".join(script) + "\n") 1520 1521 finally: 1522 package.close() 1523 1524 def _add_page_package_files(self, page_directory, prefix, package, script): 1525 1526 """ 1527 Add files in the given 'page_directory' as pages with the given 'prefix' 1528 to the given 'package', recording them in the given 'script'. 1529 """ 1530 1531 filenames = listdir(page_directory) 1532 filenames.sort() 1533 1534 for filename in filenames: 1535 pathname = join(page_directory, filename) 1536 1537 # Add files as pages having the filename as page name. 1538 1539 if os.path.isfile(pathname): 1540 pagename = prefix + filename 1541 zipname = pagename.replace("/", "__") 1542 package.write(pathname, zipname) 1543 script.append("AddRevision|%s|%s" % (zipname, pagename)) 1544 1545 elif os.path.isdir(pathname): 1546 1547 # Add directories ending with "-attachments" as collections of 1548 # attachments for a particular page. 1549 1550 if filename.endswith("-attachments"): 1551 pagename = prefix + filename[:-len("-attachments")] 1552 zipname = pagename.replace("/", "__") 1553 1554 # Add each file as an attachment. 1555 1556 for attachment in listdir(pathname): 1557 azipname = "%s_%s" % (zipname, attachment) 1558 package.write(join(pathname, attachment), azipname) 1559 script.append("AddAttachment|%s|%s|%s||" % ( 1560 azipname, attachment, pagename)) 1561 1562 # Descend into other directories. 1563 1564 else: 1565 pagename = prefix + filename 1566 self._add_page_package_files(pathname, "%s/" % pagename, package, script) 1567 1568 def install_page_package(self, package_filename): 1569 1570 """ 1571 Install a package from the file with the given 'package_filename'. 1572 """ 1573 1574 path = self._set_pythonpath() 1575 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 1576 cmd = "%s %s i %s" % (sys.executable, installer, package_filename) 1577 os.system(cmd) 1578 self._reset_pythonpath(path) 1579 1580 def show_methods(): 1581 print "Methods:" 1582 print 1583 for method_name in Installation.method_names: 1584 doc = getattr(Installation, method_name).__doc__.strip() 1585 print "%-30s%-s" % (method_name, format(doc, 30)) 1586 print 1587 1588 # Command line option syntax. 1589 1590 syntax_description = "[ -f <config-filename> ] ( -m <method> | --method=METHOD ) [ <method-argument> ... ]" 1591 1592 # Main program. 1593 1594 if __name__ == "__main__": 1595 from ConfigParser import ConfigParser 1596 import sys, cmdsyntax 1597 1598 # Check the command syntax. 1599 1600 syntax = cmdsyntax.Syntax(syntax_description) 1601 try: 1602 matches = syntax.get_args(sys.argv[1:]) 1603 args = matches[0] 1604 except IndexError: 1605 print "Syntax:" 1606 print sys.argv[0], syntax_description 1607 print 1608 show_methods() 1609 sys.exit(1) 1610 1611 # Obtain configuration details. 1612 1613 try: 1614 config_filename = args.get("config-filename", "moinsetup.cfg") 1615 1616 if not exists(config_filename): 1617 print "Configuration", config_filename, "not found." 1618 sys.exit(1) 1619 1620 config = ConfigParser() 1621 config.read(config_filename) 1622 1623 # Obtain as many arguments as needed from the configuration. 1624 1625 config_arguments = dict(config.items("installation") + config.items("site")) 1626 method_arguments = args.get("method-argument", []) 1627 1628 # Attempt to initialise the configuration. 1629 1630 installation = Installation(**config_arguments) 1631 1632 except TypeError, exc: 1633 print "Error:" 1634 print 1635 print exc.args[0] 1636 print 1637 print "Configuration settings:" 1638 print 1639 print Installation.__init__.__doc__ 1640 print 1641 sys.exit(1) 1642 1643 # Obtain the method. 1644 1645 try: 1646 method = getattr(installation, args["method"]) 1647 except AttributeError: 1648 show_methods() 1649 sys.exit(1) 1650 1651 try: 1652 method(*method_arguments) 1653 except TypeError, exc: 1654 print "Error:" 1655 print 1656 print exc.args[0] 1657 print 1658 print "Method documentation:" 1659 print 1660 print method.__doc__ 1661 print 1662 sys.exit(1) 1663 1664 # vim: tabstop=4 expandtab shiftwidth=4