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