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