source: subversion/applications/utils/import/bulkupload/upload.py @ 34714

Last change on this file since 34714 was 22742, checked in by balrog-kun, 9 years ago

Teach the upload script to work around more types of conflicts.

  • Property svn:executable set to *
File size: 16.0 KB
Line 
1#! /usr/bin/python3
2# vim: fileencoding=utf-8 encoding=utf-8 et sw=4
3
4# Copyright (C) 2009 Jacek Konieczny <jajcus@jajcus.net>
5# Copyright (C) 2009 Andrzej Zaborowski <balrogg@gmail.com>
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20
21"""
22Uploads complete osmChange 0.3 files.  Use your login (not email) as username.
23"""
24
25__version__ = "$Revision: 21 $"
26
27import os
28import subprocess
29import sys
30import traceback
31import base64
32import codecs
33
34import http.client as httplib
35import xml.etree.cElementTree as ElementTree
36import urllib.parse as urlparse
37
38class HTTPError(Exception):
39    pass
40
41class OSM_API(object):
42    url = 'http://api.openstreetmap.org/'
43    def __init__(self, username = None, password = None):
44        if username and password:
45            self.username = username
46            self.password = password
47        else:
48            self.username = ""
49            self.password = ""
50        self.changeset = None
51        self.progress_msg = None
52
53    def __del__(self):
54        #if self.changeset is not None:
55        #    self.close_changeset()
56        pass
57
58    def msg(self, mesg):
59        sys.stderr.write("\r%s…                        " % (self.progress_msg))
60        sys.stderr.write("\r%s%s" % (self.progress_msg, mesg))
61        sys.stderr.flush()
62
63    def request(self, conn, method, url, body, headers, progress):
64        if progress:
65            self.msg("making request")
66            conn.putrequest(method, url)
67            self.msg("sending headers")
68            if body:
69                conn.putheader('Content-Length', str(len(body)))
70            for hdr, value in headers.items():
71                conn.putheader(hdr, value)
72            self.msg("end of headers")
73            conn.endheaders()
74            self.msg(" 0%")
75            if body:
76                start = 0
77                size = len(body)
78                chunk = size / 100
79                if chunk < 16384:
80                    chunk = 16384
81                while start < size:
82                    end = min(size, int(start + chunk))
83                    conn.send(body[start:end])
84                    start = end
85                    self.msg("%2i%%" % (int(start * 100 / size),))
86        else:
87            self.msg(" ")
88            conn.request(method, url, body, headers)
89
90    def _run_request(self, method, url, body = None, progress = 0, content_type = "text/xml"):
91        url = urlparse.urljoin(self.url, url)
92        purl = urlparse.urlparse(url)
93        if purl.scheme != "http":
94            raise ValueError("Unsupported url scheme: %r" % (purl.scheme,))
95        if ":" in purl.netloc:
96            host, port = purl.netloc.split(":", 1)
97            port = int(port)
98        else:
99            host = purl.netloc
100            port = None
101        url = purl.path
102        if purl.query:
103            url += "?" + query
104        headers = {}
105        if body:
106            headers["Content-Type"] = content_type
107
108        try_no_auth = 0
109
110        if not try_no_auth and not self.username:
111            raise HTTPError(0, "Need a username")
112
113        try:
114            self.msg("connecting")
115            conn = httplib.HTTPConnection(host, port)
116#            conn.set_debuglevel(10)
117
118            if try_no_auth:
119                self.request(conn, method, url, body, headers, progress)
120                self.msg("waiting for status")
121                response = conn.getresponse()
122
123            if not try_no_auth or (response.status == httplib.UNAUTHORIZED and
124                    self.username):
125                if try_no_auth:
126                    conn.close()
127                    self.msg("re-connecting")
128                    conn = httplib.HTTPConnection(host, port)
129#                    conn.set_debuglevel(10)
130
131                creds = self.username + ":" + self.password
132                headers["Authorization"] = "Basic " + \
133                        base64.b64encode(bytes(creds, "utf8")).decode("utf8")
134                        # ^ Seems to be broken in python3 (even the raw
135                        # documentation examples don't run for base64)
136                self.request(conn, method, url, body, headers, progress)
137                self.msg("waiting for status")
138                response = conn.getresponse()
139
140            if response.status == httplib.OK:
141                self.msg("reading response")
142                sys.stderr.flush()
143                response_body = response.read()
144            else:
145                err = response.read()
146                raise HTTPError(response.status, "%03i: %s (%s)" % (
147                    response.status, response.reason, err), err)
148        finally:
149            conn.close()
150        return response_body
151
152    def create_changeset(self, created_by, comment):
153        if self.changeset is not None:
154            raise RuntimeError("Changeset already opened")
155        self.progress_msg = "I'm creating the changeset"
156        self.msg("")
157        root = ElementTree.Element("osm")
158        tree = ElementTree.ElementTree(root)
159        element = ElementTree.SubElement(root, "changeset")
160        ElementTree.SubElement(element, "tag", {"k": "created_by", "v": created_by})
161        ElementTree.SubElement(element, "tag", {"k": "comment", "v": comment})
162#       ElementTree.SubElement(element, "tag", {"k": "import", "v": "yes"})
163#       ElementTree.SubElement(element, "tag", {"k": "source", "v": "BDLL25, EGRN, Instituto Geográfico Nacional"})
164#       ElementTree.SubElement(element, "tag", {"k": "merged", "v": "no - possible duplicates (will be resolved in following changesets)"})
165#       ElementTree.SubElement(element, "tag", {"k": "reviewed", "v": "yes"})
166#       ElementTree.SubElement(element, "tag", {"k": "revert", "v": "yes"})
167#       ElementTree.SubElement(element, "tag", {"k": "bot", "v": "yes"})
168#       ElementTree.SubElement(element, "tag", {"k": "url", "v": "http://www.openstreetmap.org/user/nmixter/diary/8218"})
169        body = ElementTree.tostring(root, "utf-8")
170        reply = self._run_request("PUT", "/api/0.6/changeset/create", body)
171        changeset = int(reply.strip())
172        self.msg("done. Id: %i" % (changeset))
173        sys.stderr.write("\n")
174        self.changeset = changeset
175
176    def upload(self, change):
177        if self.changeset is None:
178            raise RuntimeError("Changeset not opened")
179        self.progress_msg = "Now I'm sending changes"
180        self.msg("")
181        for operation in change:
182            if operation.tag not in ("create", "modify", "delete"):
183                continue
184            for element in operation:
185                element.attrib["changeset"] = str(self.changeset)
186        body = ElementTree.tostring(change, "utf-8")
187        reply = self._run_request("POST", "/api/0.6/changeset/%i/upload"
188                                                % (self.changeset,), body, 1)
189        self.msg("done.")
190        sys.stderr.write("\n")
191        return reply
192
193    def close_changeset(self):
194        if self.changeset is None:
195            raise RuntimeError("Changeset not opened")
196        self.progress_msg = "Closing"
197        self.msg("")
198        reply = self._run_request("PUT", "/api/0.6/changeset/%i/close"
199                                                    % (self.changeset,))
200        self.changeset = None
201        self.msg("done, too.")
202        sys.stderr.write("\n")
203
204try:
205    this_dir = os.path.dirname(__file__)
206    try:
207        version = int(subprocess.Popen(["svnversion", this_dir], stdout = subprocess.PIPE).communicate()[0].strip())
208    except:
209        version = 1
210    if len(sys.argv) < 2:
211        sys.stderr.write("Synopsis:\n")
212        sys.stderr.write("    %s <file-name.osc> [<file-name.osc>...]\n" % (sys.argv[0],))
213        sys.exit(1)
214
215    filenames = []
216    param = {}
217    num = 0
218    skip = 0
219    for arg in sys.argv[1:]:
220        num += 1
221        if skip:
222            skip -= 1
223            continue
224
225        if arg == "-u":
226            param['user'] = sys.argv[num + 1]
227            skip = 1
228        elif arg == "-p":
229            param['pass'] = sys.argv[num + 1]
230            skip = 1
231        elif arg == "-c":
232            param['confirm'] = sys.argv[num + 1]
233            skip = 1
234        elif arg == "-m":
235            param['comment'] = sys.argv[num + 1]
236            skip = 1
237        elif arg == "-s":
238            param['changeset'] = sys.argv[num + 1]
239            skip = 1
240        elif arg == "-n":
241            param['start'] = 1
242            skip = 0
243        elif arg == "-t":
244            param['try'] = 1
245            skip = 0
246        else:
247            filenames.append(arg)
248
249    if 'user' in param:
250        login = param['user']
251    else:
252        login = input("OSM login: ")
253    if not login:
254        sys.exit(1)
255    if 'pass' in param:
256        password = param['pass']
257    else:
258        password = input("OSM password: ")
259    if not password:
260        sys.exit(1)
261
262    api = OSM_API(login, password)
263
264    changes = []
265    for filename in filenames:
266        if not os.path.exists(filename):
267            sys.stderr.write("File %r doesn't exist!\n" % (filename,))
268            sys.exit(1)
269        if 'start' not in param:
270            # Should still check validity, but let's save time
271
272            tree = ElementTree.parse(filename)
273            root = tree.getroot()
274            if root.tag != "osmChange" or (root.attrib.get("version") != "0.3" and
275                    root.attrib.get("version") != "0.6"):
276                sys.stderr.write("File %s is not a v0.3 osmChange file!\n" % (filename,))
277                sys.exit(1)
278
279        if filename.endswith(".osc"):
280            diff_fn = filename[:-4] + ".diff.xml"
281        else:
282            diff_fn = filename + ".diff.xml"
283        if os.path.exists(diff_fn):
284            sys.stderr.write("Diff file %r already exists, delete it " \
285                    "if you're sure you want to re-upload\n" % (diff_fn,))
286            sys.exit(1)
287
288        if filename.endswith(".osc"):
289            comment_fn = filename[:-4] + ".comment"
290        else:
291            comment_fn = filename + ".comment"
292        try:
293            comment_file = codecs.open(comment_fn, "r", "utf-8")
294            comment = comment_file.read().strip()
295            comment_file.close()
296        except IOError:
297            comment = None
298        if not comment:
299            if 'comment' in param:
300                comment = param['comment']
301            else:
302                comment = input("Your comment to %r: " % (filename,))
303            if not comment:
304                sys.exit(1)
305            #try:
306            #    comment = comment.decode(locale.getlocale()[1])
307            #except TypeError:
308            #    comment = comment.decode("UTF-8")
309
310        sys.stderr.write("     File: %r\n" % (filename,))
311        sys.stderr.write("  Comment: %s\n" % (comment,))
312
313        if 'confirm' in param:
314            sure = param['confirm']
315        else:
316            sys.stderr.write("Are you sure you want to send these changes?")
317            sure = input()
318        if sure.lower() not in ("y", "yes"):
319            sys.stderr.write("Skipping...\n\n")
320            continue
321        sys.stderr.write("\n")
322        if 'changeset' in param:
323            api.changeset = int(param['changeset'])
324        else:
325            api.create_changeset("upload.py v. %s" % (version,), comment)
326            if 'start' in param:
327                print(api.changeset)
328                sys.exit(0)
329        while 1:
330            try:
331                diff_file = codecs.open(diff_fn, "w", "utf-8")
332                diff = api.upload(root)
333                diff_file.write(diff.decode("utf8"))
334                diff_file.close()
335            except HTTPError as e:
336                sys.stderr.write("\n" + e.args[1] + "\n")
337                if e.args[0] in [ 404, 409, 412 ]: # Merge conflict
338                    # TODO: also unlink when not the whole file has been uploaded
339                    # because then likely the server will not be able to parse
340                    # it and nothing gets committed
341                    os.unlink(diff_fn)
342                errstr = e.args[2].decode("utf8")
343                if 'try' in param and e.args[0] == 409 and \
344                        errstr.find("Version mismatch") > -1:
345                    id = errstr.split(" ")[-1]
346                    found = 0
347                    for oper in root:
348                        todel = []
349                        for elem in oper:
350                            if elem.attrib.get("id") != id:
351                                continue
352                            todel.append(elem)
353                            found = 1
354                        for elem in todel:
355                            oper.remove(elem)
356                    if not found:
357                        sys.stderr.write("\nElement " + id + " not found\n")
358                        if 'changeset' not in param:
359                            api.close_changeset()
360                        sys.exit(1)
361                    sys.stderr.write("\nRetrying upload without element " +
362                            id + "\n")
363                    continue
364                if 'try' in param and e.args[0] == 400 and \
365                        errstr.find("Placeholder Way not found") > -1:
366                    id = errstr.replace(".", "").split(" ")[-1]
367                    found = 0
368                    for oper in root:
369                        todel = []
370                        for elem in oper:
371                            if elem.attrib.get("id") != id:
372                                continue
373                            todel.append(elem)
374                            found = 1
375                        for elem in todel:
376                            oper.remove(elem)
377                    if not found:
378                        sys.stderr.write("\nElement " + id + " not found\n")
379                        if 'changeset' not in param:
380                            api.close_changeset()
381                        sys.exit(1)
382                    sys.stderr.write("\nRetrying upload without element " +
383                            id + "\n")
384                    continue
385                if 'try' in param and e.args[0] == 412 and \
386                        errstr.find(" requires ") > -1:
387                    idlist = errstr.split("id in (")[1].split(")")[0].split(",")
388                    found = 0
389                    delids = []
390                    for oper in root:
391                        todel = []
392                        for elem in oper:
393                            for nd in elem:
394                                if nd.tag not in [ "nd", "member" ]:
395                                    continue
396                                if nd.attrib.get("ref") not in idlist:
397                                    continue
398                                found = 1
399                                delids.append(elem.attrib.get("id"))
400                                todel.append(elem)
401                                break
402                        for elem in todel:
403                            oper.remove(elem)
404                    if not found:
405                        sys.stderr.write("\nElement " + str(idlist) +
406                                " not found\n")
407                        if 'changeset' not in param:
408                            api.close_changeset()
409                        sys.exit(1)
410                    sys.stderr.write("\nRetrying upload without elements " +
411                            str(delids) + "\n")
412                    continue
413                if 'changeset' not in param:
414                   api.close_changeset()
415                sys.exit(1)
416            break
417        if 'changeset' not in param:
418            api.close_changeset()
419except HTTPError as err:
420    sys.stderr.write(err.args[1])
421    sys.exit(1)
422except Exception as err:
423    sys.stderr.write(repr(err) + "\n")
424    traceback.print_exc(file=sys.stderr)
425    sys.exit(1)
Note: See TracBrowser for help on using the repository browser.