blob: 266bd87fb711eaae00bc7a6d2a0cea966eb78e68 [file] [log] [blame]
# -*- coding: utf-8 -*-
#
# Copyright (C) 2003-2011 Edgewall Software
# Copyright (C) 2003-2007 Jonas Borgström <jonas@edgewall.com>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/log/.
#
# Author: Jonas Borgström <jonas@edgewall.com>
"""Trac Environment model and related APIs."""
from __future__ import with_statement
import os.path
import pkg_resources
import setuptools
import sys
from urlparse import urlsplit
from trac import db_default
from trac.admin import AdminCommandError, IAdminCommandProvider
from trac.cache import CacheManager
from trac.config import *
from trac.core import Component, ComponentManager, implements, Interface, \
ExtensionPoint, TracError
from trac.db.api import (DatabaseManager, QueryContextManager,
TransactionContextManager, with_transaction)
from trac.util import copytree, create_file, get_pkginfo, lazy, makedirs, \
read_file
from trac.util.compat import sha1
from trac.util.concurrency import threading
from trac.util.text import exception_to_unicode, path_to_unicode, printerr, \
printout
from trac.util.translation import _, N_
from trac.versioncontrol import RepositoryManager
from trac.web.href import Href
__all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment']
# Content of the VERSION file in the environment
_VERSION = 'Trac Environment Version 1'
class ISystemInfoProvider(Interface):
"""Provider of system information, displayed in the "About Trac"
page and in internal error reports.
"""
def get_system_info():
"""Yield a sequence of `(name, version)` tuples describing the
name and version information of external packages used by a
component.
"""
class IEnvironmentSetupParticipant(Interface):
"""Extension point interface for components that need to participate in
the creation and upgrading of Trac environments, for example to create
additional database tables.
Please note that `IEnvironmentSetupParticipant` instances are called in
arbitrary order. If your upgrades must be ordered consistently, please
implement the ordering in a single `IEnvironmentSetupParticipant`. See
the database upgrade infrastructure in Trac core for an example.
"""
def environment_created():
"""Called when a new Trac environment is created."""
def environment_needs_upgrade(db):
"""Called when Trac checks whether the environment needs to be
upgraded.
Should return `True` if this participant needs an upgrade to
be performed, `False` otherwise.
"""
def upgrade_environment(db):
"""Actually perform an environment upgrade.
Implementations of this method don't need to commit any
database transactions. This is done implicitly for each
participant if the upgrade succeeds without an error being
raised.
However, if the `upgrade_environment` consists of small,
restartable, steps of upgrade, it can decide to commit on its
own after each successful step.
"""
class BackupError(RuntimeError):
"""Exception raised during an upgrade when the DB backup fails."""
class Environment(Component, ComponentManager):
"""Trac environment manager.
Trac stores project information in a Trac environment. It consists
of a directory structure containing among other things:
* a configuration file,
* project-specific templates and plugins,
* the wiki and ticket attachments files,
* the SQLite database file (stores tickets, wiki pages...)
in case the database backend is sqlite
"""
implements(ISystemInfoProvider)
required = True
system_info_providers = ExtensionPoint(ISystemInfoProvider)
setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)
components_section = ConfigSection('components',
"""This section is used to enable or disable components
provided by plugins, as well as by Trac itself. The component
to enable/disable is specified via the name of the
option. Whether its enabled is determined by the option value;
setting the value to `enabled` or `on` will enable the
component, any other value (typically `disabled` or `off`)
will disable the component.
The option name is either the fully qualified name of the
components or the module/package prefix of the component. The
former enables/disables a specific component, while the latter
enables/disables any component in the specified
package/module.
Consider the following configuration snippet:
{{{
[components]
trac.ticket.report.ReportModule = disabled
webadmin.* = enabled
}}}
The first option tells Trac to disable the
[wiki:TracReports report module].
The second option instructs Trac to enable all components in
the `webadmin` package. Note that the trailing wildcard is
required for module/package matching.
To view the list of active components, go to the ''Plugins''
page on ''About Trac'' (requires `CONFIG_VIEW`
[wiki:TracPermissions permissions]).
See also: TracPlugins
""")
shared_plugins_dir = PathOption('inherit', 'plugins_dir', '',
"""Path to the //shared plugins directory//.
Plugins in that directory are loaded in addition to those in
the directory of the environment `plugins`, with this one
taking precedence.
(''since 0.11'')""")
base_url = Option('trac', 'base_url', '',
"""Reference URL for the Trac deployment.
This is the base URL that will be used when producing
documents that will be used outside of the web browsing
context, like for example when inserting URLs pointing to Trac
resources in notification e-mails.""")
base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect',
False,
"""Optionally use `[trac] base_url` for redirects.
In some configurations, usually involving running Trac behind
a HTTP proxy, Trac can't automatically reconstruct the URL
that is used to access it. You may need to use this option to
force Trac to use the `base_url` setting also for
redirects. This introduces the obvious limitation that this
environment will only be usable when accessible from that URL,
as redirects are frequently used. ''(since 0.10.5)''""")
secure_cookies = BoolOption('trac', 'secure_cookies', False,
"""Restrict cookies to HTTPS connections.
When true, set the `secure` flag on all cookies so that they
are only sent to the server on HTTPS connections. Use this if
your Trac instance is only accessible through HTTPS. (''since
0.11.2'')""")
project_name = Option('project', 'name', 'My Project',
"""Name of the project.""")
project_description = Option('project', 'descr', 'My example project',
"""Short description of the project.""")
project_url = Option('project', 'url', '',
"""URL of the main project web site, usually the website in
which the `base_url` resides. This is used in notification
e-mails.""")
project_admin = Option('project', 'admin', '',
"""E-Mail address of the project's administrator.""")
project_admin_trac_url = Option('project', 'admin_trac_url', '.',
"""Base URL of a Trac instance where errors in this Trac
should be reported.
This can be an absolute or relative URL, or '.' to reference
this Trac instance. An empty value will disable the reporting
buttons. (''since 0.11.3'')""")
project_footer = Option('project', 'footer',
N_('Visit the Trac open source project at<br />'
'<a href="http://trac.edgewall.org/">'
'http://trac.edgewall.org/</a>'),
"""Page footer text (right-aligned).""")
project_icon = Option('project', 'icon', 'common/trac.ico',
"""URL of the icon of the project.""")
log_type = Option('logging', 'log_type', 'none',
"""Logging facility to use.
Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")
log_file = Option('logging', 'log_file', 'trac.log',
"""If `log_type` is `file`, this should be a path to the
log-file. Relative paths are resolved relative to the `log`
directory of the environment.""")
log_level = Option('logging', 'log_level', 'DEBUG',
"""Level of verbosity in log.
Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")
log_format = Option('logging', 'log_format', None,
"""Custom logging format.
If nothing is set, the following will be used:
Trac[$(module)s] $(levelname)s: $(message)s
In addition to regular key names supported by the Python
logger library (see
http://docs.python.org/library/logging.html), one could use:
- $(path)s the path for the current environment
- $(basename)s the last path component of the current environment
- $(project)s the project name
Note the usage of `$(...)s` instead of `%(...)s` as the latter form
would be interpreted by the ConfigParser itself.
Example:
`($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s`
''(since 0.10.5)''""")
def __init__(self, path, create=False, options=[]):
"""Initialize the Trac environment.
:param path: the absolute path to the Trac environment
:param create: if `True`, the environment is created and
populated with default data; otherwise, the
environment is expected to already exist.
:param options: A list of `(section, name, value)` tuples that
define configuration options
"""
ComponentManager.__init__(self)
self.path = path
self.systeminfo = []
self._href = self._abs_href = None
if create:
self.create(options)
else:
self.verify()
self.setup_config()
if create:
for setup_participant in self.setup_participants:
setup_participant.environment_created()
def get_systeminfo(self):
"""Return a list of `(name, version)` tuples describing the
name and version information of external packages used by Trac
and plugins.
"""
info = self.systeminfo[:]
for provider in self.system_info_providers:
info.extend(provider.get_system_info() or [])
info.sort(key=lambda (name, version): (name != 'Trac', name.lower()))
return info
# ISystemInfoProvider methods
def get_system_info(self):
from trac import core, __version__ as VERSION
yield 'Trac', pkg_resources.resource_string('trac', 'TRAC_VERSION')
yield 'Bloodhound Trac', get_pkginfo(core).get('version', VERSION)
yield 'Python', sys.version
yield 'setuptools', setuptools.__version__
from trac.util.datefmt import pytz
if pytz is not None:
yield 'pytz', pytz.__version__
def component_activated(self, component):
"""Initialize additional member variables for components.
Every component activated through the `Environment` object
gets three member variables: `env` (the environment object),
`config` (the environment configuration) and `log` (a logger
object)."""
component.env = self
component.config = self.config
component.log = self.log
def _component_name(self, name_or_class):
name = name_or_class
if not isinstance(name_or_class, basestring):
name = name_or_class.__module__ + '.' + name_or_class.__name__
return name.lower()
@property
def _component_rules(self):
try:
return self._rules
except AttributeError:
self._rules = {}
for name, value in self.components_section.options():
if name.endswith('.*'):
name = name[:-2]
self._rules[name.lower()] = value.lower() in ('enabled', 'on')
return self._rules
def is_component_enabled(self, cls):
"""Implemented to only allow activation of components that are
not disabled in the configuration.
This is called by the `ComponentManager` base class when a
component is about to be activated. If this method returns
`False`, the component does not get activated. If it returns
`None`, the component only gets activated if it is located in
the `plugins` directory of the environment.
"""
component_name = self._component_name(cls)
# Disable the pre-0.11 WebAdmin plugin
# Please note that there's no recommendation to uninstall the
# plugin because doing so would obviously break the backwards
# compatibility that the new integration administration
# interface tries to provide for old WebAdmin extensions
if component_name.startswith('webadmin.'):
self.log.info("The legacy TracWebAdmin plugin has been "
"automatically disabled, and the integrated "
"administration interface will be used "
"instead.")
return False
rules = self._component_rules
cname = component_name
while cname:
enabled = rules.get(cname)
if enabled is not None:
return enabled
idx = cname.rfind('.')
if idx < 0:
break
cname = cname[:idx]
# By default, all components in the trac package are enabled
return component_name.startswith('trac.') or None
def enable_component(self, cls):
"""Enable a component or module."""
self._component_rules[self._component_name(cls)] = True
def verify(self):
"""Verify that the provided path points to a valid Trac environment
directory."""
try:
tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0]
if tag != _VERSION:
raise Exception("Unknown Trac environment type '%s'" % tag)
except Exception, e:
raise TracError("No Trac environment found at %s\n%s"
% (self.path, e))
def get_db_cnx(self):
"""Return a database connection from the connection pool
:deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead
`db_transaction` for obtaining the `db` database connection
which can be used for performing any query
(SELECT/INSERT/UPDATE/DELETE)::
with env.db_transaction as db:
...
Note that within the block, you don't need to (and shouldn't)
call ``commit()`` yourself, the context manager will take care
of it (if it's the outermost such context manager on the
stack).
`db_query` for obtaining a `db` database connection which can
be used for performing SELECT queries only::
with env.db_query as db:
...
"""
return DatabaseManager(self).get_connection()
@lazy
def db_exc(self):
"""Return an object (typically a module) containing all the
backend-specific exception types as attributes, named
according to the Python Database API
(http://www.python.org/dev/peps/pep-0249/).
To catch a database exception, use the following pattern::
try:
with env.db_transaction as db:
...
except env.db_exc.IntegrityError, e:
...
"""
return DatabaseManager(self).get_exceptions()
def with_transaction(self, db=None):
"""Decorator for transaction functions :deprecated:"""
return with_transaction(self, db)
def get_read_db(self):
"""Return a database connection for read purposes :deprecated:
See `trac.db.api.get_read_db` for detailed documentation."""
return DatabaseManager(self).get_connection(readonly=True)
@property
def db_query(self):
"""Return a context manager
(`~trac.db.api.QueryContextManager`) which can be used to
obtain a read-only database connection.
Example::
with env.db_query as db:
cursor = db.cursor()
cursor.execute("SELECT ...")
for row in cursor.fetchall():
...
Note that a connection retrieved this way can be "called"
directly in order to execute a query::
with env.db_query as db:
for row in db("SELECT ..."):
...
:warning: after a `with env.db_query as db` block, though the
`db` variable is still defined, you shouldn't use it as it
might have been closed when exiting the context, if this
context was the outermost context (`db_query` or
`db_transaction`).
If you don't need to manipulate the connection itself, this
can even be simplified to::
for row in env.db_query("SELECT ..."):
...
"""
return QueryContextManager(self)
@property
def db_transaction(self):
"""Return a context manager
(`~trac.db.api.TransactionContextManager`) which can be used
to obtain a writable database connection.
Example::
with env.db_transaction as db:
cursor = db.cursor()
cursor.execute("UPDATE ...")
Upon successful exit of the context, the context manager will
commit the transaction. In case of nested contexts, only the
outermost context performs a commit. However, should an
exception happen, any context manager will perform a rollback.
You should *not* call `commit()` yourself within such block,
as this will force a commit even if that transaction is part
of a larger transaction.
Like for its read-only counterpart, you can directly execute a
DML query on the `db`::
with env.db_transaction as db:
db("UPDATE ...")
:warning: after a `with env.db_transaction` as db` block,
though the `db` variable is still available, you shouldn't
use it as it might have been closed when exiting the
context, if this context was the outermost context
(`db_query` or `db_transaction`).
If you don't need to manipulate the connection itself, this
can also be simplified to::
env.db_transaction("UPDATE ...")
"""
return TransactionContextManager(self)
def shutdown(self, tid=None):
"""Close the environment."""
RepositoryManager(self).shutdown(tid)
DatabaseManager(self).shutdown(tid)
if tid is None:
self.log.removeHandler(self._log_handler)
self._log_handler.flush()
self._log_handler.close()
del self._log_handler
def get_repository(self, reponame=None, authname=None):
"""Return the version control repository with the given name,
or the default repository if `None`.
The standard way of retrieving repositories is to use the
methods of `RepositoryManager`. This method is retained here
for backward compatibility.
:param reponame: the name of the repository
:param authname: the user name for authorization (not used
anymore, left here for compatibility with
0.11)
"""
return RepositoryManager(self).get_repository(reponame)
def create(self, options=[]):
"""Create the basic directory structure of the environment,
initialize the database and populate the configuration file
with default values.
If options contains ('inherit', 'file'), default values will
not be loaded; they are expected to be provided by that file
or other options.
"""
# Create the directory structure
if not os.path.exists(self.path):
os.mkdir(self.path)
os.mkdir(self.get_log_dir())
os.mkdir(self.get_htdocs_dir())
os.mkdir(os.path.join(self.path, 'plugins'))
# Create a few files
create_file(os.path.join(self.path, 'VERSION'), _VERSION + '\n')
create_file(os.path.join(self.path, 'README'),
'This directory contains a Trac environment.\n'
'Visit http://trac.edgewall.org/ for more information.\n')
# Setup the default configuration
os.mkdir(os.path.join(self.path, 'conf'))
create_file(os.path.join(self.path, 'conf', 'trac.ini.sample'))
config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'))
for section, name, value in options:
config.set(section, name, value)
config.save()
self.setup_config()
if not any((section, option) == ('inherit', 'file')
for section, option, value in options):
self.config.set_defaults(self)
self.config.save()
# Create the database
DatabaseManager(self).init_db()
def get_version(self, db=None, initial=False):
"""Return the current version of the database. If the
optional argument `initial` is set to `True`, the version of
the database used at the time of creation will be returned.
In practice, for database created before 0.11, this will
return `False` which is "older" than any db version number.
:since: 0.11
:since 1.0: deprecation warning: the `db` parameter is no
longer used and will be removed in version 1.1.1
"""
rows = self.db_query("""
SELECT value FROM system WHERE name='%sdatabase_version'
""" % ('initial_' if initial else ''))
return rows and int(rows[0][0])
def setup_config(self):
"""Load the configuration file."""
self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'),
{'envname': os.path.basename(self.path)})
self.setup_log()
from trac.loader import load_components
plugins_dir = self.shared_plugins_dir
load_components(self, plugins_dir and (plugins_dir,))
def get_templates_dir(self):
"""Return absolute path to the templates directory."""
return os.path.join(self.path, 'templates')
def get_htdocs_dir(self):
"""Return absolute path to the htdocs directory."""
return os.path.join(self.path, 'htdocs')
def get_log_dir(self):
"""Return absolute path to the log directory."""
return os.path.join(self.path, 'log')
def setup_log(self):
"""Initialize the logging sub-system."""
from trac.log import logger_handler_factory
logtype = self.log_type
logfile = self.log_file
if logtype == 'file' and not os.path.isabs(logfile):
logfile = os.path.join(self.get_log_dir(), logfile)
format = self.log_format
logid = 'Trac.%s' % sha1(self.path).hexdigest()
if format:
format = format.replace('$(', '%(') \
.replace('%(path)s', self.path) \
.replace('%(basename)s', os.path.basename(self.path)) \
.replace('%(project)s', self.project_name)
self.log, self._log_handler = logger_handler_factory(
logtype, logfile, self.log_level, logid, format=format)
from trac import core, __version__ as VERSION
self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32,
get_pkginfo(core).get('version', VERSION))
def get_known_users(self, cnx=None):
"""Generator that yields information about all known users,
i.e. users that have logged in to this Trac environment and
possibly set their name and email.
This function generates one tuple for every user, of the form
(username, name, email) ordered alpha-numerically by username.
:param cnx: the database connection; if ommitted, a new
connection is retrieved
:since 1.0: deprecation warning: the `cnx` parameter is no
longer used and will be removed in version 1.1.1
"""
for username, name, email in self.db_query("""
SELECT DISTINCT s.sid, n.value, e.value
FROM session AS s
LEFT JOIN session_attribute AS n ON (n.sid=s.sid
and n.authenticated=1 AND n.name = 'name')
LEFT JOIN session_attribute AS e ON (e.sid=s.sid
AND e.authenticated=1 AND e.name = 'email')
WHERE s.authenticated=1 ORDER BY s.sid
"""):
yield username, name, email
def backup(self, dest=None):
"""Create a backup of the database.
:param dest: Destination file; if not specified, the backup is
stored in a file called db_name.trac_version.bak
"""
return DatabaseManager(self).backup(dest)
def needs_upgrade(self):
"""Return whether the environment needs to be upgraded."""
for participant in self.setup_participants:
with self.db_query as db:
if participant.environment_needs_upgrade(db):
self.log.warn("Component %s requires environment upgrade",
participant)
return True
return False
def upgrade(self, backup=False, backup_dest=None):
"""Upgrade database.
:param backup: whether or not to backup before upgrading
:param backup_dest: name of the backup file
:return: whether the upgrade was performed
"""
upgraders = []
for participant in self.setup_participants:
with self.db_query as db:
if participant.environment_needs_upgrade(db):
upgraders.append(participant)
if not upgraders:
return
if backup:
try:
self.backup(backup_dest)
except Exception, e:
raise BackupError(e)
for participant in upgraders:
self.log.info("%s.%s upgrading...", participant.__module__,
participant.__class__.__name__)
with self.db_transaction as db:
participant.upgrade_environment(db)
# Database schema may have changed, so close all connections
DatabaseManager(self).shutdown()
return True
@property
def href(self):
"""The application root path"""
if not self._href:
self._href = Href(urlsplit(self.abs_href.base)[2])
return self._href
@property
def abs_href(self):
"""The application URL"""
if not self._abs_href:
if not self.base_url:
self.log.warn("base_url option not set in configuration, "
"generated links may be incorrect")
self._abs_href = Href('')
else:
self._abs_href = Href(self.base_url)
return self._abs_href
class EnvironmentSetup(Component):
"""Manage automatic environment upgrades."""
required = True
implements(IEnvironmentSetupParticipant)
# IEnvironmentSetupParticipant methods
def environment_created(self):
"""Insert default data into the database."""
with self.env.db_transaction as db:
for table, cols, vals in db_default.get_data(db):
db.executemany("INSERT INTO %s (%s) VALUES (%s)"
% (table, ','.join(cols), ','.join(['%s' for c in cols])),
vals)
self._update_sample_config()
def environment_needs_upgrade(self, db):
dbver = self.env.get_version(db)
if dbver == db_default.db_version:
return False
elif dbver > db_default.db_version:
raise TracError(_('Database newer than Trac version'))
self.log.info("Trac database schema version is %d, should be %d",
dbver, db_default.db_version)
return True
def upgrade_environment(self, db):
"""Each db version should have its own upgrade module, named
upgrades/dbN.py, where 'N' is the version number (int).
"""
cursor = db.cursor()
dbver = self.env.get_version()
for i in range(dbver + 1, db_default.db_version + 1):
name = 'db%i' % i
try:
upgrades = __import__('upgrades', globals(), locals(), [name])
script = getattr(upgrades, name)
except AttributeError:
raise TracError(_("No upgrade module for version %(num)i "
"(%(version)s.py)", num=i, version=name))
script.do_upgrade(self.env, i, cursor)
cursor.execute("""
UPDATE system SET value=%s WHERE name='database_version'
""", (i,))
self.log.info("Upgraded database version from %d to %d", i - 1, i)
db.commit()
self._update_sample_config()
# Internal methods
def _update_sample_config(self):
filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample')
if not os.path.isfile(filename):
return
config = Configuration(filename)
for section, default_options in config.defaults().iteritems():
for name, value in default_options.iteritems():
config.set(section, name, value)
try:
config.save()
self.log.info("Wrote sample configuration file with the new "
"settings and their default values: %s",
filename)
except IOError, e:
self.log.warn("Couldn't write sample configuration file (%s)", e,
exc_info=True)
env_cache = {}
env_cache_lock = threading.Lock()
def open_environment(env_path=None, use_cache=False):
"""Open an existing environment object, and verify that the database is up
to date.
:param env_path: absolute path to the environment directory; if
ommitted, the value of the `TRAC_ENV` environment
variable is used
:param use_cache: whether the environment should be cached for
subsequent invocations of this function
:return: the `Environment` object
"""
if not env_path:
env_path = os.getenv('TRAC_ENV')
if not env_path:
raise TracError(_('Missing environment variable "TRAC_ENV". '
'Trac requires this variable to point to a valid '
'Trac environment.'))
env_path = os.path.normcase(os.path.normpath(env_path))
if use_cache:
with env_cache_lock:
env = env_cache.get(env_path)
if env and env.config.parse_if_needed():
# The environment configuration has changed, so shut it down
# and remove it from the cache so that it gets reinitialized
env.log.info('Reloading environment due to configuration '
'change')
env.shutdown()
del env_cache[env_path]
env = None
if env is None:
env = env_cache.setdefault(env_path, open_environment(env_path))
else:
CacheManager(env).reset_metadata()
else:
env = Environment(env_path)
needs_upgrade = False
try:
needs_upgrade = env.needs_upgrade()
except Exception, e: # e.g. no database connection
env.log.error("Exception caught while checking for upgrade: %s",
exception_to_unicode(e, traceback=True))
if needs_upgrade:
raise TracError(_('The Trac Environment needs to be upgraded.\n\n'
'Run "trac-admin %(path)s upgrade"',
path=env_path))
return env
class EnvironmentAdmin(Component):
"""trac-admin command provider for environment administration."""
implements(IAdminCommandProvider)
# IAdminCommandProvider methods
def get_admin_commands(self):
yield ('deploy', '<directory>',
'Extract static resources from Trac and all plugins',
None, self._do_deploy)
yield ('hotcopy', '<backupdir> [--no-database]',
"""Make a hot backup copy of an environment
The database is backed up to the 'db' directory of the
destination, unless the --no-database option is
specified.
""",
None, self._do_hotcopy)
yield ('upgrade', '',
'Upgrade database to current version',
None, self._do_upgrade)
def _do_deploy(self, dest):
target = os.path.normpath(dest)
chrome_target = os.path.join(target, 'htdocs')
script_target = os.path.join(target, 'cgi-bin')
# Copy static content
makedirs(target, overwrite=True)
makedirs(chrome_target, overwrite=True)
from trac.web.chrome import Chrome
printout(_("Copying resources from:"))
for provider in Chrome(self.env).template_providers:
paths = list(provider.get_htdocs_dirs() or [])
if not len(paths):
continue
printout(' %s.%s' % (provider.__module__,
provider.__class__.__name__))
for key, root in paths:
if not root:
continue
source = os.path.normpath(root)
printout(' ', source)
if os.path.exists(source):
dest = os.path.join(chrome_target, key)
copytree(source, dest, overwrite=True)
# Create and copy scripts
makedirs(script_target, overwrite=True)
printout(_("Creating scripts."))
data = {'env': self.env, 'executable': sys.executable}
for script in ('cgi', 'fcgi', 'wsgi'):
dest = os.path.join(script_target, 'trac.' + script)
template = Chrome(self.env).load_template('deploy_trac.' + script,
'text')
stream = template.generate(**data)
with open(dest, 'w') as out:
stream.render('text', out=out, encoding='utf-8')
def _do_hotcopy(self, dest, no_db=None):
if no_db not in (None, '--no-database'):
raise AdminCommandError(_("Invalid argument '%(arg)s'", arg=no_db),
show_usage=True)
if os.path.exists(dest):
raise TracError(_("hotcopy can't overwrite existing '%(dest)s'",
dest=path_to_unicode(dest)))
import shutil
# Bogus statement to lock the database while copying files
with self.env.db_transaction as db:
db("UPDATE system SET name=NULL WHERE name IS NULL")
printout(_("Hotcopying %(src)s to %(dst)s ...",
src=path_to_unicode(self.env.path),
dst=path_to_unicode(dest)))
db_str = self.env.config.get('trac', 'database')
prefix, db_path = db_str.split(':', 1)
skip = []
if prefix == 'sqlite':
db_path = os.path.join(self.env.path, os.path.normpath(db_path))
# don't copy the journal (also, this would fail on Windows)
skip = [db_path + '-journal', db_path + '-stmtjrnl']
if no_db:
skip.append(db_path)
try:
copytree(self.env.path, dest, symlinks=1, skip=skip)
retval = 0
except shutil.Error, e:
retval = 1
printerr(_("The following errors happened while copying "
"the environment:"))
for (src, dst, err) in e.args[0]:
if src in err:
printerr(' %s' % err)
else:
printerr(" %s: '%s'" % (err, path_to_unicode(src)))
# db backup for non-sqlite
if prefix != 'sqlite' and not no_db:
printout(_("Backing up database ..."))
sql_backup = os.path.join(dest, 'db',
'%s-db-backup.sql' % prefix)
self.env.backup(sql_backup)
printout(_("Hotcopy done."))
return retval
def _do_upgrade(self, no_backup=None):
if no_backup not in (None, '-b', '--no-backup'):
raise AdminCommandError(_("Invalid arguments"), show_usage=True)
if not self.env.needs_upgrade():
printout(_("Database is up to date, no upgrade necessary."))
return
try:
self.env.upgrade(backup=no_backup is None)
except BackupError, e:
printerr(_("The pre-upgrade backup failed.\nUse '--no-backup' to "
"upgrade without doing a backup.\n"))
raise e.args[0]
except Exception, e:
printerr(_("The upgrade failed. Please fix the issue and try "
"again.\n"))
raise
# Remove wiki-macros if it is empty and warn if it isn't
wiki_macros = os.path.join(self.env.path, 'wiki-macros')
try:
entries = os.listdir(wiki_macros)
except OSError:
pass
else:
if entries:
printerr(_("Warning: the wiki-macros directory in the "
"environment is non-empty, but Trac\n"
"doesn't load plugins from there anymore. "
"Please remove it by hand."))
else:
try:
os.rmdir(wiki_macros)
except OSError, e:
printerr(_("Error while removing wiki-macros: %(err)s\n"
"Trac doesn't load plugins from wiki-macros "
"anymore. Please remove it by hand.",
err=exception_to_unicode(e)))
printout(_("Upgrade done.\n\n"
"You may want to upgrade the Trac documentation now by "
"running:\n\n trac-admin %(path)s wiki upgrade",
path=path_to_unicode(self.env.path)))