aboutsummaryrefslogtreecommitdiff
path: root/upload/upload.py
diff options
context:
space:
mode:
Diffstat (limited to 'upload/upload.py')
-rwxr-xr-xupload/upload.py432
1 files changed, 432 insertions, 0 deletions
diff --git a/upload/upload.py b/upload/upload.py
new file mode 100755
index 0000000..934bc7b
--- /dev/null
+++ b/upload/upload.py
@@ -0,0 +1,432 @@
+#! /usr/bin/python3
+# vim: fileencoding=utf-8 encoding=utf-8 et sw=4
+
+# Copyright (C) 2009 Jacek Konieczny <jajcus@jajcus.net>
+# Copyright (C) 2009 Andrzej Zaborowski <balrogg@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+"""
+Uploads complete osmChange 0.3 files. Use your login (not email) as username.
+"""
+
+__version__ = "$Revision: 21 $"
+
+import os
+import subprocess
+import sys
+import traceback
+import base64
+import codecs
+
+import http.client as httplib
+import xml.etree.cElementTree as ElementTree
+import urllib.parse as urlparse
+
+class HTTPError(Exception):
+ pass
+
+class OSM_API(object):
+ url = 'https://master.apis.dev.openstreetmap.org/'
+ def __init__(self, username = None, password = None):
+ if username and password:
+ self.username = username
+ self.password = password
+ else:
+ self.username = ""
+ self.password = ""
+ self.changeset = None
+ self.progress_msg = None
+
+ def __del__(self):
+ #if self.changeset is not None:
+ # self.close_changeset()
+ pass
+
+ def msg(self, mesg):
+ sys.stderr.write("\r%s… " % (self.progress_msg))
+ sys.stderr.write("\r%s… %s" % (self.progress_msg, mesg))
+ sys.stderr.flush()
+
+ def request(self, conn, method, url, body, headers, progress):
+ if progress:
+ self.msg("making request")
+ conn.putrequest(method, url)
+ self.msg("sending headers")
+ if body:
+ conn.putheader('Content-Length', str(len(body)))
+ for hdr, value in headers.items():
+ conn.putheader(hdr, value)
+ self.msg("end of headers")
+ conn.endheaders()
+ self.msg(" 0%")
+ if body:
+ start = 0
+ size = len(body)
+ chunk = size / 100
+ if chunk < 16384:
+ chunk = 16384
+ while start < size:
+ end = min(size, int(start + chunk))
+ conn.send(body[start:end])
+ start = end
+ self.msg("%2i%%" % (int(start * 100 / size),))
+ else:
+ self.msg(" ")
+ conn.request(method, url, body, headers)
+
+ def _run_request(self, method, url, body = None, progress = 0, content_type = "text/xml"):
+ url = urlparse.urljoin(self.url, url)
+ purl = urlparse.urlparse(url)
+ if purl.scheme != "https":
+ raise ValueError("Unsupported url scheme: %r" % (purl.scheme,))
+ if ":" in purl.netloc:
+ host, port = purl.netloc.split(":", 1)
+ port = int(port)
+ else:
+ host = purl.netloc
+ port = None
+ url = purl.path
+ if purl.query:
+ url += "?" + query
+ headers = {}
+ if body:
+ headers["Content-Type"] = content_type
+
+ try_no_auth = 0
+
+ if not try_no_auth and not self.username:
+ raise HTTPError(0, "Need a username")
+
+ try:
+ self.msg("connecting")
+ conn = httplib.HTTPSConnection(host, port)
+# conn.set_debuglevel(10)
+
+ if try_no_auth:
+ self.request(conn, method, url, body, headers, progress)
+ self.msg("waiting for status")
+ response = conn.getresponse()
+
+ if not try_no_auth or (response.status == httplib.UNAUTHORIZED and
+ self.username):
+ if try_no_auth:
+ conn.close()
+ self.msg("re-connecting")
+ conn = httplib.HTTPSConnection(host, port)
+# conn.set_debuglevel(10)
+
+ creds = self.username + ":" + self.password
+ headers["Authorization"] = "Basic " + \
+ base64.b64encode(bytes(creds, "utf8")).decode("utf8")
+ # ^ Seems to be broken in python3 (even the raw
+ # documentation examples don't run for base64)
+ self.request(conn, method, url, body, headers, progress)
+ self.msg("waiting for status")
+ response = conn.getresponse()
+
+ if response.status == httplib.OK:
+ self.msg("reading response")
+ sys.stderr.flush()
+ response_body = response.read()
+ else:
+ err = response.read()
+ raise HTTPError(response.status, "%03i: %s (%s)" % (
+ response.status, response.reason, err), err)
+ finally:
+ conn.close()
+ return response_body
+
+ def create_changeset(self, created_by, comment, source):
+ if self.changeset is not None:
+ raise RuntimeError("Changeset already opened")
+ self.progress_msg = "I'm creating the changeset"
+ self.msg("")
+ root = ElementTree.Element("osm")
+ tree = ElementTree.ElementTree(root)
+ element = ElementTree.SubElement(root, "changeset")
+ ElementTree.SubElement(element, "tag", {"k": "comment", "v": comment})
+ ElementTree.SubElement(element, "tag", {"k": "source", "v": source})
+ ElementTree.SubElement(element, "tag", {"k": "source:ref", "v": "https://www.land.vic.gov.au/maps-and-spatial/spatial-data/vicmap-catalogue/vicmap-address"})
+ ElementTree.SubElement(element, "tag", {"k": "created_by", "v": created_by})
+ body = ElementTree.tostring(root, "utf-8")
+ reply = self._run_request("PUT", "/api/0.6/changeset/create", body)
+ changeset = int(reply.strip())
+ self.msg("done.\nChangeset ID: %i" % (changeset))
+ sys.stderr.write("\n")
+ self.changeset = changeset
+
+ def upload(self, change):
+ if self.changeset is None:
+ raise RuntimeError("Changeset not opened")
+ self.progress_msg = "Now I'm sending changes"
+ self.msg("")
+ for operation in change:
+ if operation.tag not in ("create", "modify", "delete"):
+ continue
+ for element in operation:
+ element.attrib["changeset"] = str(self.changeset)
+ body = ElementTree.tostring(change, "utf-8")
+ reply = self._run_request("POST", "/api/0.6/changeset/%i/upload"
+ % (self.changeset,), body, 1)
+ self.msg("done.")
+ sys.stderr.write("\n")
+ return reply
+
+ def close_changeset(self):
+ if self.changeset is None:
+ raise RuntimeError("Changeset not opened")
+ self.progress_msg = "Closing"
+ self.msg("")
+ reply = self._run_request("PUT", "/api/0.6/changeset/%i/close"
+ % (self.changeset,))
+ self.changeset = None
+ self.msg("done, too.")
+ sys.stderr.write("\n")
+
+try:
+ this_dir = os.path.dirname(__file__)
+ try:
+ version = int(subprocess.Popen(["svnversion", this_dir], stdout = subprocess.PIPE).communicate()[0].strip())
+ except:
+ version = 1
+ if len(sys.argv) < 2:
+ sys.stderr.write("Synopsis:\n")
+ sys.stderr.write(" %s <file-name.osc> [<file-name.osc>...]\n" % (sys.argv[0],))
+ sys.exit(1)
+
+ filenames = []
+ param = {}
+ num = 0
+ skip = 0
+ for arg in sys.argv[1:]:
+ num += 1
+ if skip:
+ skip -= 1
+ continue
+
+ if arg == "-u":
+ param['user'] = sys.argv[num + 1]
+ skip = 1
+ elif arg == "-p":
+ param['pass'] = sys.argv[num + 1]
+ skip = 1
+ elif arg == "-c":
+ param['confirm'] = sys.argv[num + 1]
+ skip = 1
+ elif arg == "-m":
+ param['comment'] = sys.argv[num + 1]
+ skip = 1
+ elif arg == "-s":
+ param['changeset'] = sys.argv[num + 1]
+ skip = 1
+ elif arg == "-n":
+ param['start'] = 1
+ skip = 0
+ elif arg == "-t":
+ param['try'] = 1
+ skip = 0
+ elif arg == "-x":
+ param['created_by'] = sys.argv[num + 1]
+ skip = 1
+ elif arg == "-y":
+ param['source'] = sys.argv[num + 1]
+ skip = 1
+ elif arg == "-z":
+ param['url'] = sys.argv[num + 1]
+ skip = 1
+ else:
+ filenames.append(arg)
+
+ if 'user' in param:
+ login = param['user']
+ else:
+ login = input("OSM login: ")
+ if not login:
+ sys.exit(1)
+ if 'pass' in param:
+ password = param['pass']
+ else:
+ password = input("OSM password: ")
+ if not password:
+ sys.exit(1)
+
+ api = OSM_API(login, password)
+
+ changes = []
+ for filename in filenames:
+ if not os.path.exists(filename):
+ sys.stderr.write("File %r doesn't exist!\n" % (filename,))
+ sys.exit(1)
+ if 'start' not in param:
+ # Should still check validity, but let's save time
+
+ tree = ElementTree.parse(filename)
+ root = tree.getroot()
+ if root.tag != "osmChange" or (root.attrib.get("version") != "0.3" and
+ root.attrib.get("version") != "0.6"):
+ sys.stderr.write("File %s is not a v0.3 osmChange file!\n" % (filename,))
+ sys.exit(1)
+
+ if filename.endswith(".osc"):
+ diff_fn = filename[:-4] + ".diff.xml"
+ else:
+ diff_fn = filename + ".diff.xml"
+ if os.path.exists(diff_fn):
+ sys.stderr.write("Diff file %r already exists, delete it " \
+ "if you're sure you want to re-upload\n" % (diff_fn,))
+ sys.exit(1)
+
+ if filename.endswith(".osc"):
+ comment_fn = filename[:-4] + ".comment"
+ else:
+ comment_fn = filename + ".comment"
+ try:
+ comment_file = codecs.open(comment_fn, "r", "utf-8")
+ comment = comment_file.read().strip()
+ comment_file.close()
+ except IOError:
+ comment = None
+ if not comment:
+ if 'comment' in param:
+ comment = param['comment']
+ else:
+ comment = input("Your comment to %r: " % (filename,))
+ if not comment:
+ sys.exit(1)
+ #try:
+ # comment = comment.decode(locale.getlocale()[1])
+ #except TypeError:
+ # comment = comment.decode("UTF-8")
+
+ sys.stderr.write(" File: %r\n" % (filename,))
+ sys.stderr.write(" Comment: %s\n" % (comment,))
+
+ if 'confirm' in param:
+ sure = param['confirm']
+ else:
+ sys.stderr.write("Are you sure you want to send these changes?")
+ sure = input()
+ if sure.lower() not in ("y", "yes"):
+ sys.stderr.write("Skipping...\n\n")
+ continue
+ sys.stderr.write("\n")
+ created_by = param.get("created_by", "osm-bulk-upload/upload.py v. %s" % (version,))
+ source = param.get("source", "")
+ url = param.get("url", "")
+ if 'changeset' in param:
+ api.changeset = int(param['changeset'])
+ else:
+ api.create_changeset(created_by, comment, source, url)
+ if 'start' in param:
+ print(api.changeset)
+ sys.exit(0)
+ while 1:
+ try:
+ diff_file = codecs.open(diff_fn, "w", "utf-8")
+ diff = api.upload(root)
+ diff_file.write(diff.decode("utf8"))
+ diff_file.close()
+ except HTTPError as e:
+ sys.stderr.write("\n" + e.args[1] + "\n")
+ if e.args[0] in [ 404, 409, 412 ]: # Merge conflict
+ # TODO: also unlink when not the whole file has been uploaded
+ # because then likely the server will not be able to parse
+ # it and nothing gets committed
+ os.unlink(diff_fn)
+ errstr = e.args[2].decode("utf8")
+ if 'try' in param and e.args[0] == 409 and \
+ errstr.find("Version mismatch") > -1:
+ id = errstr.split(" ")[-1]
+ found = 0
+ for oper in root:
+ todel = []
+ for elem in oper:
+ if elem.attrib.get("id") != id:
+ continue
+ todel.append(elem)
+ found = 1
+ for elem in todel:
+ oper.remove(elem)
+ if not found:
+ sys.stderr.write("\nElement " + id + " not found\n")
+ if 'changeset' not in param:
+ api.close_changeset()
+ sys.exit(1)
+ sys.stderr.write("\nRetrying upload without element " +
+ id + "\n")
+ continue
+ if 'try' in param and e.args[0] == 400 and \
+ errstr.find("Placeholder Way not found") > -1:
+ id = errstr.replace(".", "").split(" ")[-1]
+ found = 0
+ for oper in root:
+ todel = []
+ for elem in oper:
+ if elem.attrib.get("id") != id:
+ continue
+ todel.append(elem)
+ found = 1
+ for elem in todel:
+ oper.remove(elem)
+ if not found:
+ sys.stderr.write("\nElement " + id + " not found\n")
+ if 'changeset' not in param:
+ api.close_changeset()
+ sys.exit(1)
+ sys.stderr.write("\nRetrying upload without element " +
+ id + "\n")
+ continue
+ if 'try' in param and e.args[0] == 412 and \
+ errstr.find(" requires ") > -1:
+ idlist = errstr.split("id in (")[1].split(")")[0].split(",")
+ found = 0
+ delids = []
+ for oper in root:
+ todel = []
+ for elem in oper:
+ for nd in elem:
+ if nd.tag not in [ "nd", "member" ]:
+ continue
+ if nd.attrib.get("ref") not in idlist:
+ continue
+ found = 1
+ delids.append(elem.attrib.get("id"))
+ todel.append(elem)
+ break
+ for elem in todel:
+ oper.remove(elem)
+ if not found:
+ sys.stderr.write("\nElement " + str(idlist) +
+ " not found\n")
+ if 'changeset' not in param:
+ api.close_changeset()
+ sys.exit(1)
+ sys.stderr.write("\nRetrying upload without elements " +
+ str(delids) + "\n")
+ continue
+ if 'changeset' not in param:
+ api.close_changeset()
+ sys.exit(1)
+ break
+ if 'changeset' not in param:
+ api.close_changeset()
+except HTTPError as err:
+ sys.stderr.write(err.args[1])
+ sys.exit(1)
+except Exception as err:
+ sys.stderr.write(repr(err) + "\n")
+ traceback.print_exc(file=sys.stderr)
+ sys.exit(1)