Ticket #2927: migrate-tracblog5.py

File migrate-tracblog5.py, 8.4 kB (added by risto.kankkunen@iki.fi, 6 months ago)

Migrate also the blog attachments

Line 
1 #!/bin/env python
2
3 # -*- coding: utf-8 -*-
4 # Copyright (C) 2008 John Hampton <pacopablo@pacopablo.com>
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
9 # are met:
10 #
11 # 1. Redistributions of source code must retain the above copyright
12 #    notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
14 #    notice, this list of conditions and the following disclaimer in
15 #    the documentation and/or other materials provided with the
16 #    distribution.
17 # 3. The name of the author may not be used to endorse or promote
18 #    products derived from this software without specific prior
19 #    written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR `AS IS'' AND ANY EXPRESS
22 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
25 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
27 # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
29 # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
30 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
33 # Author: John Hampton <pacopablo@pacopablo.com>
34
35 import sys
36 import time
37 import re
38 import os
39 from optparse import OptionParser
40
41 from trac.env import Environment
42 from trac.wiki.model import WikiPage
43 from trac.test import Mock, MockPerm
44 from trac.attachment import Attachment
45
46
47 _title_split_match = re.compile(r'\s*^=+\s+([^\n\r=]+?)\s+=+\s+(.+)$',
48                                 re.MULTILINE|re.DOTALL).match
49
50 _tag_split = re.compile('[,\s]+')
51
52 def split_tags(tags):
53     """ Split the tags into a list """
54     return  [t.strip() for t in _tag_split.split(tags) if t.strip()]
55
56 def epochtime(t):
57     """ Return seconds from epoch from a datetime object """
58     return int(time.mktime(t.timetuple()))
59
60 def insert_blog_post(cnx, name, version, title, body, publish_time,
61                      version_time, version_comment, version_author,
62                      author, categories):
63     """ Insert the post into the FullBlog tables """
64     cur = cnx.cursor()
65     try:
66         cur.execute("INSERT INTO fullblog_posts "
67                     "(name, version, title, body, publish_time, version_time, "
68                     "version_comment, version_author, author, categories) "
69                     "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
70                     (name, version, title, body, epochtime(publish_time),
71                     epochtime(version_time), version_comment, version_author,
72                     author, categories))
73     except Exception, e:
74         print("Unable to insert %s into the FullBlog: %s" % (name, e))
75         raise
76
77 def reparent_blog_attachments(env, orig_name, new_name):
78     """ Re-associate blog post attachments to FullBlog posts """
79     cnx = env.get_db_cnx()
80     cur = cnx.cursor()
81
82     new_dir = Attachment(env, 'blog', new_name).path
83
84     attachment_paths = list()
85     for attachment in Attachment.select(env, 'wiki', orig_name):
86         if os.path.exists(os.path.join(new_dir, attachment.filename)):
87             print "Attachment", attachment.filename, "already where it should be"
88             continue
89         if not os.path.exists(attachment.path):
90             raise Exception("Cannot find attachment %s for post %s" % (
91                 attachment.filename, orig_name
92             ))
93         attachment_paths.append(attachment.path)
94
95     try:
96         cur.execute(
97             "UPDATE attachment "
98             "SET type = 'blog', id = %s "
99             "WHERE type = 'wiki' AND id = %s",
100             (new_name, orig_name)
101         )
102     except Exception, e:
103         print("Unable to import blog attachment %s into the FullBlog: %s" % (orig_name, e))
104         raise
105
106     if not attachment_paths:
107         return
108
109     for p in attachment_paths:
110         try:
111             new_p = os.path.join(new_dir, os.path.basename(p))
112             print "Moving attachment"
113             print "   from", p
114             print "   to  ", new_p
115             os.renames(p, new_p)
116         except Exception, e:
117             print "Failed to move %s: %s" % (p, e)
118             raise
119
120 def Main(opts):
121     """ Cross your fingers and pray """
122     env = Environment(opts.envpath)
123     from tractags.api import TagSystem
124
125     tlist = opts.tags or split_tags(env.config.get('blog', 'default_tag',
126                                                    'blog'))
127     tags = TagSystem(env)
128     req = Mock(perm=MockPerm())
129     blog = tags.query(req, ' '.join(tlist + ['realm:wiki']))
130                    
131     cnx = env.get_db_cnx()
132     for resource, page_tags in list(blog):
133         try:
134             page = WikiPage(env, version=1, name=resource.id)
135             _, publish_time, author, _, _ =  page.get_history().next()
136             if opts.deleteonly:
137                 page.delete()
138                 continue
139             categories = ' '.join([t for t in page_tags if t not in tlist])
140             page = WikiPage(env, name=resource.id)
141             for version, version_time, version_author, version_comment, \
142                 _ in page.get_history():
143                 # Currently the basename of the post url is used due to
144                 # http://trac-hacks.org/ticket/2956
145                 name = resource.id.replace('/', '_')
146                 # extract title from text:
147                 fulltext = page.text
148                 match = _title_split_match(fulltext)
149                 if match:
150                     title = match.group(1)
151                     fulltext = match.group(2)
152                 else:
153                     title = name
154                 body = fulltext
155                 print "Adding post %s, v%s: %s" % (name, version, title)
156                 insert_blog_post(cnx, name, version, title, body,
157                                  publish_time, version_time,
158                                  version_comment, version_author, author,
159                                  categories)
160                 reparent_blog_attachments(env, resource.id, name)
161                 continue
162             cnx.commit()
163             if opts.delete:
164                 page.delete()
165                 continue
166         except:
167             env.log.debug("Error loading wiki page %s" % resource.id,
168                           exc_info=True)
169             print "Failed to add post %s, v%s: %s" % (name, version, title)
170             cnx.rollback()
171             cnx.close()
172             return 1
173     cnx.close()
174     return 0
175    
176
177 def doArgs():
178     """Parse command line options"""
179     description = "%prog is used to migrate posts from TracBlogPlugin to " \
180                   "FullBlogPlugin."
181
182     parser = OptionParser(usage="usage: %prog [options] [environment]",
183                           version="1.0", description=description)
184     parser.add_option("-d", "--delete", dest="delete", action="store_true",
185                       help="Delete the TracBlog posts from the wiki after "
186                       "migration", default=False)
187     parser.add_option("", "--delete-only", dest="deleteonly",
188                       action="store_true", help="Only delete the TracBlog "
189                       "posts from the wiki.  Do not perform any migration "
190                       "steps", default=False)
191     parser.add_option("-t", "--tags", dest="tags", type="string",
192                       help="Comma separated list of tags specifying blog "
193                       "posts.  If not specified, the `default_tag` value "
194                       "from trac.ini is used.", metavar="<list>",
195                       default=None)
196     (options, args) = parser.parse_args()
197     if len(args) < 1:
198         print("You must specify a Trac environment")
199         sys.exit(1)
200     options.envpath = args[0]
201     if not os.path.exists(options.envpath):
202         print("The path >%s< does not exist.  Please specify an existing "
203               "path." % options.envpath)
204         sys.exit(1)
205     options.args = args
206     return options
207
208 if __name__ == '__main__':
209     options = doArgs()
210     rc = Main(options)
211     sys.exit(rc)