Package trac :: Module env

Source Code for Module trac.env

   1  # -*- coding: utf-8 -*- 
   2  # 
   3  # Copyright (C) 2003-2020 Edgewall Software 
   4  # Copyright (C) 2003-2007 Jonas Borgström <[email protected]> 
   5  # All rights reserved. 
   6  # 
   7  # This software is licensed as described in the file COPYING, which 
   8  # you should have received as part of this distribution. The terms 
   9  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
  10  # 
  11  # This software consists of voluntary contributions made by many 
  12  # individuals. For the exact contribution history, see the revision 
  13  # history and logs, available at https://trac.edgewall.org/log/. 
  14  # 
  15  # Author: Jonas Borgström <[email protected]> 
  16   
  17  """Trac Environment model and related APIs.""" 
  18   
  19  from __future__ import with_statement 
  20   
  21  import os.path 
  22  import setuptools 
  23  import sys 
  24  from urlparse import urlsplit 
  25   
  26  from trac import db_default 
  27  from trac.admin import AdminCommandError, IAdminCommandProvider 
  28  from trac.cache import CacheManager, cached 
  29  from trac.config import BoolOption, ConfigSection, Configuration, IntOption, \ 
  30                          Option, PathOption 
  31  from trac.core import Component, ComponentManager, implements, Interface, \ 
  32                        ExtensionPoint, TracError 
  33  from trac.db.api import (DatabaseManager, QueryContextManager, 
  34                           TransactionContextManager, with_transaction) 
  35  from trac.util import copytree, create_file, get_pkginfo, lazy, makedirs, \ 
  36                        read_file 
  37  from trac.util.compat import sha1 
  38  from trac.util.concurrency import threading 
  39  from trac.util.text import exception_to_unicode, path_to_unicode, printerr, \ 
  40                             printout 
  41  from trac.util.translation import _, N_ 
  42  from trac.web.href import Href 
  43   
  44  __all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment'] 
  45   
  46   
  47  # Content of the VERSION file in the environment 
  48  _VERSION = 'Trac Environment Version 1' 
49 50 51 -class ISystemInfoProvider(Interface):
52 """Provider of system information, displayed in the "About Trac" 53 page and in internal error reports. 54 """
55 - def get_system_info():
56 """Yield a sequence of `(name, version)` tuples describing the 57 name and version information of external packages used by a 58 component. 59 """
60
61 62 -class IEnvironmentSetupParticipant(Interface):
63 """Extension point interface for components that need to participate in 64 the creation and upgrading of Trac environments, for example to create 65 additional database tables. 66 67 Please note that `IEnvironmentSetupParticipant` instances are called in 68 arbitrary order. If your upgrades must be ordered consistently, please 69 implement the ordering in a single `IEnvironmentSetupParticipant`. See 70 the database upgrade infrastructure in Trac core for an example. 71 """ 72
74 """Called when a new Trac environment is created."""
75
77 """Called when Trac checks whether the environment needs to be 78 upgraded. 79 80 Should return `True` if this participant needs an upgrade to 81 be performed, `False` otherwise. 82 """
83
84 - def upgrade_environment(db):
85 """Actually perform an environment upgrade. 86 87 Implementations of this method don't need to commit any 88 database transactions. This is done implicitly for each 89 participant if the upgrade succeeds without an error being 90 raised. 91 92 However, if the `upgrade_environment` consists of small, 93 restartable, steps of upgrade, it can decide to commit on its 94 own after each successful step. 95 """
96
97 98 -class BackupError(RuntimeError):
99 """Exception raised during an upgrade when the DB backup fails."""
100
101 102 -class Environment(Component, ComponentManager):
103 """Trac environment manager. 104 105 Trac stores project information in a Trac environment. It consists 106 of a directory structure containing among other things: 107 108 * a configuration file, 109 * project-specific templates and plugins, 110 * the wiki and ticket attachments files, 111 * the SQLite database file (stores tickets, wiki pages...) 112 in case the database backend is sqlite 113 114 """ 115 116 implements(ISystemInfoProvider) 117 118 required = True 119 120 system_info_providers = ExtensionPoint(ISystemInfoProvider) 121 setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) 122 123 components_section = ConfigSection('components', 124 """This section is used to enable or disable components 125 provided by plugins, as well as by Trac itself. The component 126 to enable/disable is specified via the name of the 127 option. Whether its enabled is determined by the option value; 128 setting the value to `enabled` or `on` will enable the 129 component, any other value (typically `disabled` or `off`) 130 will disable the component. 131 132 The option name is either the fully qualified name of the 133 components or the module/package prefix of the component. The 134 former enables/disables a specific component, while the latter 135 enables/disables any component in the specified 136 package/module. 137 138 Consider the following configuration snippet: 139 {{{ 140 [components] 141 trac.ticket.report.ReportModule = disabled 142 webadmin.* = enabled 143 }}} 144 145 The first option tells Trac to disable the 146 [wiki:TracReports report module]. 147 The second option instructs Trac to enable all components in 148 the `webadmin` package. Note that the trailing wildcard is 149 required for module/package matching. 150 151 To view the list of active components, go to the ''Plugins'' 152 page on ''About Trac'' (requires `CONFIG_VIEW` 153 [wiki:TracPermissions permissions]). 154 155 See also: TracPlugins 156 """) 157 158 shared_plugins_dir = PathOption('inherit', 'plugins_dir', '', 159 """Path to the //shared plugins directory//. 160 161 Plugins in that directory are loaded in addition to those in 162 the directory of the environment `plugins`, with this one 163 taking precedence. 164 165 Non-absolute paths are relative to the Environment `conf` 166 directory. 167 (''since 0.11'')""") 168 169 base_url = Option('trac', 'base_url', '', 170 """Reference URL for the Trac deployment. 171 172 This is the base URL that will be used when producing 173 documents that will be used outside of the web browsing 174 context, like for example when inserting URLs pointing to Trac 175 resources in notification e-mails.""") 176 177 base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect', 178 False, 179 """Optionally use `[trac] base_url` for redirects. 180 181 In some configurations, usually involving running Trac behind 182 a HTTP proxy, Trac can't automatically reconstruct the URL 183 that is used to access it. You may need to use this option to 184 force Trac to use the `base_url` setting also for 185 redirects. This introduces the obvious limitation that this 186 environment will only be usable when accessible from that URL, 187 as redirects are frequently used. ''(since 0.10.5)''""") 188 189 secure_cookies = BoolOption('trac', 'secure_cookies', False, 190 """Restrict cookies to HTTPS connections. 191 192 When true, set the `secure` flag on all cookies so that they 193 are only sent to the server on HTTPS connections. Use this if 194 your Trac instance is only accessible through HTTPS. (''since 195 0.11.2'')""") 196 197 anonymous_session_lifetime = IntOption( 198 'trac', 'anonymous_session_lifetime', '90', 199 """Lifetime of the anonymous session, in days. 200 201 Set the option to 0 to disable purging old anonymous sessions. 202 (''since 1.0.17'')""") 203 204 project_name = Option('project', 'name', 'My Project', 205 """Name of the project.""") 206 207 project_description = Option('project', 'descr', 'My example project', 208 """Short description of the project.""") 209 210 project_url = Option('project', 'url', '', 211 """URL of the main project web site, usually the website in 212 which the `base_url` resides. This is used in notification 213 e-mails.""") 214 215 project_admin = Option('project', 'admin', '', 216 """E-Mail address of the project's administrator.""") 217 218 project_admin_trac_url = Option('project', 'admin_trac_url', '.', 219 """Base URL of a Trac instance where errors in this Trac 220 should be reported. 221 222 This can be an absolute or relative URL, or '.' to reference 223 this Trac instance. An empty value will disable the reporting 224 buttons. (''since 0.11.3'')""") 225 226 project_footer = Option('project', 'footer', 227 N_('Visit the Trac open source project at<br />' 228 '<a href="https://trac.edgewall.org/">' 229 'https://trac.edgewall.org/</a>'), 230 """Page footer text (right-aligned).""") 231 232 project_icon = Option('project', 'icon', 'common/trac.ico', 233 """URL of the icon of the project.""") 234 235 log_type = Option('logging', 'log_type', 'none', 236 """Logging facility to use. 237 238 Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""") 239 240 log_file = Option('logging', 'log_file', 'trac.log', 241 """If `log_type` is `file`, this should be a path to the 242 log-file. Relative paths are resolved relative to the `log` 243 directory of the environment.""") 244 245 log_level = Option('logging', 'log_level', 'DEBUG', 246 """Level of verbosity in log. 247 248 Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""") 249 250 log_format = Option('logging', 'log_format', None, 251 """Custom logging format. 252 253 If nothing is set, the following will be used: 254 255 Trac[$(module)s] $(levelname)s: $(message)s 256 257 In addition to regular key names supported by the Python 258 logger library (see 259 http://docs.python.org/library/logging.html), one could use: 260 261 - $(path)s the path for the current environment 262 - $(basename)s the last path component of the current environment 263 - $(project)s the project name 264 265 Note the usage of `$(...)s` instead of `%(...)s` as the latter form 266 would be interpreted by the ConfigParser itself. 267 268 Example: 269 `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s` 270 271 ''(since 0.10.5)''""") 272
273 - def __init__(self, path, create=False, options=[]):
274 """Initialize the Trac environment. 275 276 :param path: the absolute path to the Trac environment 277 :param create: if `True`, the environment is created and 278 populated with default data; otherwise, the 279 environment is expected to already exist. 280 :param options: A list of `(section, name, value)` tuples that 281 define configuration options 282 """ 283 ComponentManager.__init__(self) 284 285 self.path = path 286 self.systeminfo = [] 287 288 if create: 289 self.create(options) 290 else: 291 self.verify() 292 self.setup_config() 293 294 if create: 295 for setup_participant in self.setup_participants: 296 setup_participant.environment_created()
297 298 @property
299 - def env(self):
300 """Property returning the `Environment` object, which is often 301 required for functions and methods that take a `Component` instance. 302 """ 303 # The cached decorator requires the object have an `env` attribute. 304 return self
305
306 - def get_systeminfo(self):
307 """Return a list of `(name, version)` tuples describing the 308 name and version information of external packages used by Trac 309 and plugins. 310 """ 311 info = self.systeminfo[:] 312 for provider in self.system_info_providers: 313 info.extend(provider.get_system_info() or []) 314 return sorted(set(info), 315 key=lambda (name, ver): (name != 'Trac', name.lower()))
316 317 # ISystemInfoProvider methods 318
319 - def get_system_info(self):
320 from trac import core, __version__ as VERSION 321 yield 'Trac', get_pkginfo(core).get('version', VERSION) 322 yield 'Python', sys.version 323 yield 'setuptools', setuptools.__version__ 324 from trac.util.datefmt import pytz 325 if pytz is not None: 326 yield 'pytz', pytz.__version__
327
328 - def component_activated(self, component):
329 """Initialize additional member variables for components. 330 331 Every component activated through the `Environment` object 332 gets three member variables: `env` (the environment object), 333 `config` (the environment configuration) and `log` (a logger 334 object).""" 335 component.env = self 336 component.config = self.config 337 component.log = self.log
338
339 - def _component_name(self, name_or_class):
340 name = name_or_class 341 if not isinstance(name_or_class, basestring): 342 name = name_or_class.__module__ + '.' + name_or_class.__name__ 343 return name.lower()
344 345 @lazy
346 - def _component_rules(self):
347 _rules = {} 348 for name, value in self.components_section.options(): 349 if name.endswith('.*'): 350 name = name[:-2] 351 _rules[name.lower()] = value.lower() in ('enabled', 'on') 352 return _rules
353
354 - def is_component_enabled(self, cls):
355 """Implemented to only allow activation of components that are 356 not disabled in the configuration. 357 358 This is called by the `ComponentManager` base class when a 359 component is about to be activated. If this method returns 360 `False`, the component does not get activated. If it returns 361 `None`, the component only gets activated if it is located in 362 the `plugins` directory of the environment. 363 """ 364 component_name = self._component_name(cls) 365 366 # Disable the pre-0.11 WebAdmin plugin 367 # Please note that there's no recommendation to uninstall the 368 # plugin because doing so would obviously break the backwards 369 # compatibility that the new integration administration 370 # interface tries to provide for old WebAdmin extensions 371 if component_name.startswith('webadmin.'): 372 self.log.info("The legacy TracWebAdmin plugin has been " 373 "automatically disabled, and the integrated " 374 "administration interface will be used " 375 "instead.") 376 return False 377 378 rules = self._component_rules 379 cname = component_name 380 while cname: 381 enabled = rules.get(cname) 382 if enabled is not None: 383 return enabled 384 idx = cname.rfind('.') 385 if idx < 0: 386 break 387 cname = cname[:idx] 388 389 # By default, all components in the trac package except 390 # trac.test are enabled 391 return component_name.startswith('trac.') and \ 392 not component_name.startswith('trac.test.') or None
393
394 - def enable_component(self, cls):
395 """Enable a component or module.""" 396 self._component_rules[self._component_name(cls)] = True 397 super(Environment, self).enable_component(cls)
398
399 - def verify(self):
400 """Verify that the provided path points to a valid Trac environment 401 directory.""" 402 try: 403 tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0] 404 if tag != _VERSION: 405 raise Exception(_("Unknown Trac environment type '%(type)s'", 406 type=tag)) 407 except Exception, e: 408 raise TracError(_("No Trac environment found at %(path)s\n" 409 "%(e)s", path=self.path, e=e))
410
411 - def get_db_cnx(self):
412 """Return a database connection from the connection pool 413 414 :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead. 415 Removed in Trac 1.1.2. 416 417 `db_transaction` for obtaining the `db` database connection 418 which can be used for performing any query 419 (SELECT/INSERT/UPDATE/DELETE):: 420 421 with env.db_transaction as db: 422 ... 423 424 Note that within the block, you don't need to (and shouldn't) 425 call ``commit()`` yourself, the context manager will take care 426 of it (if it's the outermost such context manager on the 427 stack). 428 429 430 `db_query` for obtaining a `db` database connection which can 431 be used for performing SELECT queries only:: 432 433 with env.db_query as db: 434 ... 435 """ 436 return DatabaseManager(self).get_connection()
437 438 @lazy
439 - def db_exc(self):
440 """Return an object (typically a module) containing all the 441 backend-specific exception types as attributes, named 442 according to the Python Database API 443 (http://www.python.org/dev/peps/pep-0249/). 444 445 To catch a database exception, use the following pattern:: 446 447 try: 448 with env.db_transaction as db: 449 ... 450 except env.db_exc.IntegrityError, e: 451 ... 452 """ 453 return DatabaseManager(self).get_exceptions()
454
455 - def with_transaction(self, db=None):
456 """Decorator for transaction functions. 457 458 :deprecated: Use the query and transaction context managers instead. 459 Will be removed in Trac 1.3.1. 460 """ 461 return with_transaction(self, db)
462
463 - def get_read_db(self):
464 """Return a database connection for read purposes. 465 466 See `trac.db.api.get_read_db` for detailed documentation. 467 468 :deprecated: Use :meth:`db_query` instead. 469 Will be removed in Trac 1.3.1. 470 """ 471 return DatabaseManager(self).get_connection(readonly=True)
472 473 @property
474 - def db_query(self):
475 """Return a context manager 476 (`~trac.db.api.QueryContextManager`) which can be used to 477 obtain a read-only database connection. 478 479 Example:: 480 481 with env.db_query as db: 482 cursor = db.cursor() 483 cursor.execute("SELECT ...") 484 for row in cursor.fetchall(): 485 ... 486 487 Note that a connection retrieved this way can be "called" 488 directly in order to execute a query:: 489 490 with env.db_query as db: 491 for row in db("SELECT ..."): 492 ... 493 494 :warning: after a `with env.db_query as db` block, though the 495 `db` variable is still defined, you shouldn't use it as it 496 might have been closed when exiting the context, if this 497 context was the outermost context (`db_query` or 498 `db_transaction`). 499 500 If you don't need to manipulate the connection itself, this 501 can even be simplified to:: 502 503 for row in env.db_query("SELECT ..."): 504 ... 505 506 """ 507 return QueryContextManager(self)
508 509 @property
510 - def db_transaction(self):
511 """Return a context manager 512 (`~trac.db.api.TransactionContextManager`) which can be used 513 to obtain a writable database connection. 514 515 Example:: 516 517 with env.db_transaction as db: 518 cursor = db.cursor() 519 cursor.execute("UPDATE ...") 520 521 Upon successful exit of the context, the context manager will 522 commit the transaction. In case of nested contexts, only the 523 outermost context performs a commit. However, should an 524 exception happen, any context manager will perform a rollback. 525 You should *not* call `commit()` yourself within such block, 526 as this will force a commit even if that transaction is part 527 of a larger transaction. 528 529 Like for its read-only counterpart, you can directly execute a 530 DML query on the `db`:: 531 532 with env.db_transaction as db: 533 db("UPDATE ...") 534 535 :warning: after a `with env.db_transaction` as db` block, 536 though the `db` variable is still available, you shouldn't 537 use it as it might have been closed when exiting the 538 context, if this context was the outermost context 539 (`db_query` or `db_transaction`). 540 541 If you don't need to manipulate the connection itself, this 542 can also be simplified to:: 543 544 env.db_transaction("UPDATE ...") 545 546 """ 547 return TransactionContextManager(self)
548
549 - def shutdown(self, tid=None):
550 """Close the environment.""" 551 from trac.versioncontrol.api import RepositoryManager 552 RepositoryManager(self).shutdown(tid) 553 DatabaseManager(self).shutdown(tid) 554 if tid is None: 555 self.log.removeHandler(self._log_handler) 556 self._log_handler.flush() 557 self._log_handler.close() 558 del self._log_handler
559
560 - def get_repository(self, reponame=None, authname=None):
561 """Return the version control repository with the given name, 562 or the default repository if `None`. 563 564 The standard way of retrieving repositories is to use the 565 methods of `RepositoryManager`. This method is retained here 566 for backward compatibility. 567 568 :param reponame: the name of the repository 569 :param authname: the user name for authorization (not used 570 anymore, left here for compatibility with 571 0.11) 572 """ 573 from trac.versioncontrol.api import RepositoryManager 574 return RepositoryManager(self).get_repository(reponame)
575
576 - def create(self, options=[]):
577 """Create the basic directory structure of the environment, 578 initialize the database and populate the configuration file 579 with default values. 580 581 If options contains ('inherit', 'file'), default values will 582 not be loaded; they are expected to be provided by that file 583 or other options. 584 """ 585 # Create the directory structure 586 if not os.path.exists(self.path): 587 os.mkdir(self.path) 588 os.mkdir(self.log_dir) 589 os.mkdir(self.htdocs_dir) 590 os.mkdir(self.plugins_dir) 591 592 # Create a few files 593 create_file(os.path.join(self.path, 'VERSION'), _VERSION + '\n') 594 create_file(os.path.join(self.path, 'README'), 595 'This directory contains a Trac environment.\n' 596 'Visit https://trac.edgewall.org/ for more information.\n') 597 598 # Setup the default configuration 599 os.mkdir(self.conf_dir) 600 create_file(os.path.join(self.conf_dir, 'trac.ini.sample')) 601 config = Configuration(os.path.join(self.conf_dir, 'trac.ini')) 602 for section, name, value in options: 603 config.set(section, name, value) 604 config.save() 605 self.setup_config() 606 if not any((section, option) == ('inherit', 'file') 607 for section, option, value in options): 608 self.config.set_defaults(self) 609 self.config.save() 610 611 # Create the database 612 DatabaseManager(self).init_db()
613 614 @lazy
615 - def database_version(self):
616 """Returns the current version of the database. 617 618 :since 1.0.2: 619 """ 620 return self.get_version()
621 622 @lazy
623 - def database_initial_version(self):
624 """Returns the version of the database at the time of creation. 625 626 In practice, for database created before 0.11, this will 627 return `False` which is "older" than any db version number. 628 629 :since 1.0.2: 630 """ 631 return self.get_version(initial=True)
632
633 - def get_version(self, db=None, initial=False):
634 """Return the current version of the database. If the 635 optional argument `initial` is set to `True`, the version of 636 the database used at the time of creation will be returned. 637 638 In practice, for database created before 0.11, this will 639 return `False` which is "older" than any db version number. 640 641 :since: 0.11 642 643 :since 1.0: deprecation warning: the `db` parameter is no 644 longer used and will be removed in version 1.1.1 645 646 :since 1.0.2: The lazily-evaluated attributes `database_version` and 647 `database_initial_version` should be used instead. This 648 method will be renamed to a private method in 649 release 1.3.1. 650 """ 651 with self.db_query as db: 652 rows = db(""" 653 SELECT value FROM %s WHERE name='%sdatabase_version' 654 """ % (db.quote('system'), 'initial_' if initial else '')) 655 return int(rows[0][0]) if rows else False
656
657 - def setup_config(self):
658 """Load the configuration file.""" 659 config_file_path = os.path.join(self.conf_dir, 'trac.ini') 660 self.config = Configuration(config_file_path, 661 {'envname': os.path.basename(self.path)}) 662 if not self.config.exists: 663 raise TracError(_("The configuration file is not found at " 664 "%(path)s", path=config_file_path)) 665 self.setup_log() 666 from trac.loader import load_components 667 plugins_dir = self.shared_plugins_dir 668 load_components(self, plugins_dir and (plugins_dir,))
669
670 - def _get_path_to_dir(self, dir):
671 path = os.path.join(self.path, dir) 672 return os.path.normcase(os.path.realpath(path))
673 674 @lazy
675 - def conf_dir(self):
676 """Absolute path to the conf directory. 677 678 :since: 1.0.11 679 """ 680 return self._get_path_to_dir('conf')
681 682 @lazy
683 - def htdocs_dir(self):
684 """Absolute path to the htdocs directory. 685 686 :since: 1.0.11 687 """ 688 return self._get_path_to_dir('htdocs')
689
690 - def get_htdocs_dir(self):
691 """Return absolute path to the htdocs directory. 692 693 :since 1.0.11: Deprecated and will be removed in 1.3.1. Use the 694 `htdocs_dir` property instead. 695 """ 696 return self._get_path_to_dir('htdocs')
697 698 @lazy
699 - def log_dir(self):
700 """Absolute path to the log directory. 701 702 :since: 1.0.11 703 """ 704 return self._get_path_to_dir('log')
705
706 - def get_log_dir(self):
707 """Return absolute path to the log directory. 708 709 :since 1.0.11: Deprecated and will be removed in 1.3.1. Use the 710 `log_dir` property instead. 711 """ 712 return self._get_path_to_dir('log')
713 714 @lazy
715 - def plugins_dir(self):
716 """Absolute path to the plugins directory. 717 718 :since: 1.0.11 719 """ 720 return self._get_path_to_dir('plugins')
721 722 @lazy
723 - def templates_dir(self):
724 """Absolute path to the templates directory. 725 726 :since: 1.0.11 727 """ 728 return self._get_path_to_dir('templates')
729
730 - def get_templates_dir(self):
731 """Return absolute path to the templates directory. 732 733 :since 1.0.11: Deprecated and will be removed in 1.3.1. Use the 734 `templates_dir` property instead. 735 """ 736 return self._get_path_to_dir('templates')
737
738 - def setup_log(self):
739 """Initialize the logging sub-system.""" 740 from trac.log import logger_handler_factory 741 logtype = self.log_type 742 logfile = self.log_file 743 if logtype == 'file' and not os.path.isabs(logfile): 744 logfile = os.path.join(self.log_dir, logfile) 745 format = self.log_format 746 logid = 'Trac.%s' % sha1(self.path).hexdigest() 747 if format: 748 format = format.replace('$(', '%(') \ 749 .replace('%(path)s', self.path) \ 750 .replace('%(basename)s', os.path.basename(self.path)) \ 751 .replace('%(project)s', self.project_name) 752 self.log, self._log_handler = logger_handler_factory( 753 logtype, logfile, self.log_level, logid, format=format) 754 from trac import core, __version__ as VERSION 755 self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32, 756 get_pkginfo(core).get('version', VERSION))
757
758 - def get_known_users(self, cnx=None):
759 """Generator that yields information about all known users, 760 i.e. users that have logged in to this Trac environment and 761 possibly set their name and email. 762 763 This function generates one tuple for every user, of the form 764 (username, name, email) ordered alpha-numerically by username. 765 766 :param cnx: the database connection; if ommitted, a new 767 connection is retrieved 768 769 :since 1.0: deprecation warning: the `cnx` parameter is no 770 longer used and will be removed in version 1.1.1 771 """ 772 return iter(self._known_users)
773 774 @cached
775 - def _known_users(self):
776 return self.db_query(""" 777 SELECT DISTINCT s.sid, n.value, e.value 778 FROM session AS s 779 LEFT JOIN session_attribute AS n ON (n.sid=s.sid 780 AND n.authenticated=1 AND n.name = 'name') 781 LEFT JOIN session_attribute AS e ON (e.sid=s.sid 782 AND e.authenticated=1 AND e.name = 'email') 783 WHERE s.authenticated=1 ORDER BY s.sid 784 """)
785
787 """Clear the known_users cache.""" 788 del self._known_users
789
790 - def backup(self, dest=None):
791 """Create a backup of the database. 792 793 :param dest: Destination file; if not specified, the backup is 794 stored in a file called db_name.trac_version.bak 795 """ 796 return DatabaseManager(self).backup(dest)
797
798 - def needs_upgrade(self):
799 """Return whether the environment needs to be upgraded.""" 800 for participant in self.setup_participants: 801 with self.db_query as db: 802 if participant.environment_needs_upgrade(db): 803 self.log.warn("Component %s requires environment upgrade", 804 participant) 805 return True 806 return False
807
808 - def upgrade(self, backup=False, backup_dest=None):
809 """Upgrade database. 810 811 :param backup: whether or not to backup before upgrading 812 :param backup_dest: name of the backup file 813 :return: whether the upgrade was performed 814 """ 815 upgraders = [] 816 for participant in self.setup_participants: 817 with self.db_query as db: 818 if participant.environment_needs_upgrade(db): 819 upgraders.append(participant) 820 if not upgraders: 821 return 822 823 if backup: 824 try: 825 self.backup(backup_dest) 826 except Exception, e: 827 raise BackupError(e) 828 829 for participant in upgraders: 830 self.log.info("%s.%s upgrading...", participant.__module__, 831 participant.__class__.__name__) 832 with self.db_transaction as db: 833 participant.upgrade_environment(db) 834 # Database schema may have changed, so close all connections 835 DatabaseManager(self).shutdown() 836 del self.database_version 837 return True
838 839 @lazy
840 - def href(self):
841 """The application root path""" 842 return Href(urlsplit(self.abs_href.base).path)
843 844 @lazy
845 - def abs_href(self):
846 """The application URL""" 847 if not self.base_url: 848 self.log.warn("[trac] base_url option not set in configuration, " 849 "generated links may be incorrect") 850 return Href(self.base_url)
851
852 853 -class EnvironmentSetup(Component):
854 """Manage automatic environment upgrades.""" 855 856 required = True 857 858 implements(IEnvironmentSetupParticipant) 859 860 # IEnvironmentSetupParticipant methods 861
862 - def environment_created(self):
863 """Insert default data into the database.""" 864 with self.env.db_transaction as db: 865 for table, cols, vals in db_default.get_data(db): 866 db.executemany("INSERT INTO %s (%s) VALUES (%s)" 867 % (db.quote(table), ','.join(cols), 868 ','.join(['%s' for c in cols])), 869 vals) 870 self._update_sample_config()
871
872 - def environment_needs_upgrade(self, db):
873 dbver = self.env.database_version 874 if dbver == db_default.db_version: 875 return False 876 elif dbver > db_default.db_version: 877 raise TracError(_('Database newer than Trac version')) 878 self.log.info("Trac database schema version is %d, should be %d", 879 dbver, db_default.db_version) 880 return True
881
882 - def upgrade_environment(self, db):
883 """Each db version should have its own upgrade module, named 884 upgrades/dbN.py, where 'N' is the version number (int). 885 """ 886 cursor = db.cursor() 887 update_stmt = """ 888 UPDATE %s SET value=%%s WHERE name='database_version' 889 """ % db.quote('system') 890 dbver = self.env.database_version 891 for i in range(dbver + 1, db_default.db_version + 1): 892 name = 'db%i' % i 893 try: 894 upgrades = __import__('upgrades', globals(), locals(), [name]) 895 script = getattr(upgrades, name) 896 except AttributeError: 897 raise TracError(_("No upgrade module for version %(num)i " 898 "(%(version)s.py)", num=i, version=name)) 899 script.do_upgrade(self.env, i, cursor) 900 cursor.execute(update_stmt, (i,)) 901 self.log.info("Upgraded database version from %d to %d", i - 1, i) 902 db.commit() 903 self._update_sample_config()
904 905 # Internal methods 906
907 - def _update_sample_config(self):
908 filename = os.path.join(self.env.conf_dir, 'trac.ini.sample') 909 if not os.path.isfile(filename): 910 return 911 config = Configuration(filename) 912 for (section, name), option in Option.get_registry().iteritems(): 913 config.set(section, name, option.dumps(option.default)) 914 try: 915 config.save() 916 self.log.info("Wrote sample configuration file with the new " 917 "settings and their default values: %s", 918 filename) 919 except EnvironmentError, e: 920 self.log.warn("Couldn't write sample configuration file (%s)", e, 921 exc_info=True)
922 923 924 env_cache = {} 925 env_cache_lock = threading.Lock()
926 927 -def open_environment(env_path=None, use_cache=False):
928 """Open an existing environment object, and verify that the database is up 929 to date. 930 931 :param env_path: absolute path to the environment directory; if 932 ommitted, the value of the `TRAC_ENV` environment 933 variable is used 934 :param use_cache: whether the environment should be cached for 935 subsequent invocations of this function 936 :return: the `Environment` object 937 """ 938 if not env_path: 939 env_path = os.getenv('TRAC_ENV') 940 if not env_path: 941 raise TracError(_('Missing environment variable "TRAC_ENV". ' 942 'Trac requires this variable to point to a valid ' 943 'Trac environment.')) 944 945 env_path = os.path.normcase(os.path.normpath(env_path)) 946 if use_cache: 947 with env_cache_lock: 948 env = env_cache.get(env_path) 949 if env and env.config.parse_if_needed(): 950 # The environment configuration has changed, so shut it down 951 # and remove it from the cache so that it gets reinitialized 952 env.log.info('Reloading environment due to configuration ' 953 'change') 954 env.shutdown() 955 del env_cache[env_path] 956 env = None 957 if env is None: 958 env = env_cache.setdefault(env_path, open_environment(env_path)) 959 else: 960 CacheManager(env).reset_metadata() 961 else: 962 env = Environment(env_path) 963 needs_upgrade = False 964 try: 965 needs_upgrade = env.needs_upgrade() 966 except Exception, e: # e.g. no database connection 967 env.log.error("Exception caught while checking for upgrade: %s", 968 exception_to_unicode(e, traceback=True)) 969 if needs_upgrade: 970 raise TracError(_('The Trac Environment needs to be upgraded. ' 971 'Run:\n\n trac-admin "%(path)s" upgrade', 972 path=env_path)) 973 974 return env
975
976 977 -class EnvironmentAdmin(Component):
978 """trac-admin command provider for environment administration.""" 979 980 implements(IAdminCommandProvider) 981 982 # IAdminCommandProvider methods 983
984 - def get_admin_commands(self):
985 yield ('deploy', '<directory>', 986 'Extract static resources from Trac and all plugins', 987 None, self._do_deploy) 988 yield ('hotcopy', '<backupdir> [--no-database]', 989 """Make a hot backup copy of an environment 990 991 The database is backed up to the 'db' directory of the 992 destination, unless the --no-database option is 993 specified. 994 """, 995 None, self._do_hotcopy) 996 yield ('upgrade', '', 997 'Upgrade database to current version', 998 None, self._do_upgrade)
999
1000 - def _do_deploy(self, dest):
1001 target = os.path.normpath(dest) 1002 chrome_target = os.path.join(target, 'htdocs') 1003 script_target = os.path.join(target, 'cgi-bin') 1004 1005 # Copy static content 1006 makedirs(target, overwrite=True) 1007 makedirs(chrome_target, overwrite=True) 1008 from trac.web.chrome import Chrome 1009 printout(_("Copying resources from:")) 1010 for provider in Chrome(self.env).template_providers: 1011 paths = list(provider.get_htdocs_dirs() or []) 1012 if not len(paths): 1013 continue 1014 printout(' %s.%s' % (provider.__module__, 1015 provider.__class__.__name__)) 1016 for key, root in paths: 1017 if not root: 1018 continue 1019 source = os.path.normpath(root) 1020 printout(' ', source) 1021 if os.path.exists(source): 1022 dest = os.path.join(chrome_target, key) 1023 copytree(source, dest, overwrite=True) 1024 1025 # Create and copy scripts 1026 makedirs(script_target, overwrite=True) 1027 printout(_("Creating scripts.")) 1028 data = {'env': self.env, 'executable': sys.executable} 1029 for script in ('cgi', 'fcgi', 'wsgi'): 1030 dest = os.path.join(script_target, 'trac.' + script) 1031 template = Chrome(self.env).load_template('deploy_trac.' + script, 1032 'text') 1033 stream = template.generate(**data) 1034 with open(dest, 'w') as out: 1035 stream.render('text', out=out, encoding='utf-8')
1036
1037 - def _do_hotcopy(self, dest, no_db=None):
1038 if no_db not in (None, '--no-database'): 1039 raise AdminCommandError(_("Invalid argument '%(arg)s'", arg=no_db), 1040 show_usage=True) 1041 1042 if os.path.exists(dest): 1043 raise TracError(_("hotcopy can't overwrite existing '%(dest)s'", 1044 dest=path_to_unicode(dest))) 1045 import shutil 1046 1047 # Bogus statement to lock the database while copying files 1048 with self.env.db_transaction as db: 1049 db("UPDATE " + db.quote('system') + 1050 " SET name=NULL WHERE name IS NULL") 1051 1052 printout(_("Hotcopying %(src)s to %(dst)s ...", 1053 src=path_to_unicode(self.env.path), 1054 dst=path_to_unicode(dest))) 1055 db_str = self.env.config.get('trac', 'database') 1056 prefix, db_path = db_str.split(':', 1) 1057 skip = [] 1058 1059 if prefix == 'sqlite': 1060 db_path = os.path.join(self.env.path, os.path.normpath(db_path)) 1061 # don't copy the journal (also, this would fail on Windows) 1062 skip = [db_path + '-journal', db_path + '-stmtjrnl', 1063 db_path + '-shm', db_path + '-wal'] 1064 if no_db: 1065 skip.append(db_path) 1066 1067 try: 1068 copytree(self.env.path, dest, symlinks=1, skip=skip) 1069 retval = 0 1070 except shutil.Error, e: 1071 retval = 1 1072 printerr(_("The following errors happened while copying " 1073 "the environment:")) 1074 for (src, dst, err) in e.args[0]: 1075 if src in err: 1076 printerr(' %s' % err) 1077 else: 1078 printerr(" %s: '%s'" % (err, path_to_unicode(src))) 1079 1080 1081 # db backup for non-sqlite 1082 if prefix != 'sqlite' and not no_db: 1083 printout(_("Backing up database ...")) 1084 sql_backup = os.path.join(dest, 'db', 1085 '%s-db-backup.sql' % prefix) 1086 self.env.backup(sql_backup) 1087 1088 printout(_("Hotcopy done.")) 1089 return retval
1090
1091 - def _do_upgrade(self, no_backup=None):
1092 if no_backup not in (None, '-b', '--no-backup'): 1093 raise AdminCommandError(_("Invalid arguments"), show_usage=True) 1094 1095 if not self.env.needs_upgrade(): 1096 printout(_("Database is up to date, no upgrade necessary.")) 1097 return 1098 1099 try: 1100 self.env.upgrade(backup=no_backup is None) 1101 except BackupError, e: 1102 printerr(_("The pre-upgrade backup failed.\nUse '--no-backup' to " 1103 "upgrade without doing a backup.\n")) 1104 raise e.args[0] 1105 except Exception, e: 1106 printerr(_("The upgrade failed. Please fix the issue and try " 1107 "again.\n")) 1108 raise 1109 1110 # Remove wiki-macros if it is empty and warn if it isn't 1111 wiki_macros = os.path.join(self.env.path, 'wiki-macros') 1112 try: 1113 entries = os.listdir(wiki_macros) 1114 except OSError: 1115 pass 1116 else: 1117 if entries: 1118 printerr(_("Warning: the wiki-macros directory in the " 1119 "environment is non-empty, but Trac\n" 1120 "doesn't load plugins from there anymore. " 1121 "Please remove it by hand.")) 1122 else: 1123 try: 1124 os.rmdir(wiki_macros) 1125 except OSError, e: 1126 printerr(_("Error while removing wiki-macros: %(err)s\n" 1127 "Trac doesn't load plugins from wiki-macros " 1128 "anymore. Please remove it by hand.", 1129 err=exception_to_unicode(e))) 1130 1131 printout(_('Upgrade done.\n\n' 1132 'You may want to upgrade the Trac documentation now by ' 1133 'running:\n\n trac-admin "%(path)s" wiki upgrade', 1134 path=path_to_unicode(self.env.path)))
1135