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