added these for convenience
[otp-base.git] / bootstrap_creator / googlecode_upload.py
blobd81b7e0921ebeb3ffc14e178bbfb9d79799a0c41
1 #!/usr/bin/env python
3 # Copyright 2006, 2007 Google Inc. All Rights Reserved.
4 # Author: danderson@google.com (David Anderson)
6 # Script for uploading files to a Google Code project.
8 # This is intended to be both a useful script for people who want to
9 # streamline project uploads and a reference implementation for
10 # uploading files to Google Code projects.
12 # To upload a file to Google Code, you need to provide a path to the
13 # file on your local machine, a small summary of what the file is, a
14 # project name, and a valid account that is a member or owner of that
15 # project. You can optionally provide a list of labels that apply to
16 # the file. The file will be uploaded under the same name that it has
17 # in your local filesystem (that is, the "basename" or last path
18 # component). Run the script with '--help' to get the exact syntax
19 # and available options.
21 # Note that the upload script requests that you enter your
22 # googlecode.com password. This is NOT your Gmail account password!
23 # This is the password you use on googlecode.com for committing to
24 # Subversion and uploading files. You can find your password by going
25 # to http://code.google.com/hosting/settings when logged in with your
26 # Gmail account. If you have already committed to your project's
27 # Subversion repository, the script will automatically retrieve your
28 # credentials from there (unless disabled, see the output of '--help'
29 # for details).
31 # If you are looking at this script as a reference for implementing
32 # your own Google Code file uploader, then you should take a look at
33 # the upload() function, which is the meat of the uploader. You
34 # basically need to build a multipart/form-data POST request with the
35 # right fields and send it to https://PROJECT.googlecode.com/files .
36 # Authenticate the request using HTTP Basic authentication, as is
37 # shown below.
39 # Licensed under the terms of the Apache Software License 2.0:
40 # http://www.apache.org/licenses/LICENSE-2.0
42 # Questions, comments, feature requests and patches are most welcome.
43 # Please direct all of these to the Google Code users group:
44 # http://groups.google.com/group/google-code-hosting
46 """Google Code file uploader script.
47 """
49 __author__ = 'danderson@google.com (David Anderson)'
51 import httplib
52 import os.path
53 import optparse
54 import getpass
55 import base64
56 import sys
59 def get_svn_config_dir():
60 """Return user's Subversion configuration directory."""
61 try:
62 from win32com.shell.shell import SHGetFolderPath
63 import win32com.shell.shellcon
64 except ImportError:
65 # If we can't import the win32api, just use ~; this is right on unix, and
66 # returns not entirely unreasonable results on Windows.
67 return os.path.expanduser('~/.subversion')
69 # We're on Windows with win32api; use APPDATA.
70 return os.path.join(SHGetFolderPath(0, win32com.shell.shellcon.CSIDL_APPDATA,
71 0, 0).encode('utf-8'),
72 'Subversion')
75 def get_svn_auth(project_name, config_dir):
76 """Return (username, password) for project_name in config_dir."""
78 # Default to returning nothing.
79 result = (None, None)
81 try:
82 from svn.core import SVN_AUTH_CRED_SIMPLE, svn_config_read_auth_data
83 from svn.core import SubversionException
84 except ImportError:
85 return result
87 realm = ('<https://%s.googlecode.com:443> Google Code Subversion Repository'
88 % project_name)
90 # auth may be none even if no exception is raised, e.g. if config_dir does
91 # not exist, or exists but has no entry for realm.
92 try:
93 auth = svn_config_read_auth_data(SVN_AUTH_CRED_SIMPLE, realm, config_dir)
94 except SubversionException:
95 auth = None
97 if auth is not None:
98 try:
99 result = (auth['username'], auth['password'])
100 except KeyError:
101 # Missing the keys, so return nothing.
102 pass
104 return result
107 def upload(file, project_name, user_name, password, summary, labels=None):
108 """Upload a file to a Google Code project's file server.
110 Args:
111 file: The local path to the file.
112 project_name: The name of your project on Google Code.
113 user_name: Your Google account name.
114 password: The googlecode.com password for your account.
115 Note that this is NOT your global Google Account password!
116 summary: A small description for the file.
117 labels: an optional list of label strings with which to tag the file.
119 Returns: a tuple:
120 http_status: 201 if the upload succeeded, something else if an
121 error occured.
122 http_reason: The human-readable string associated with http_status
123 file_url: If the upload succeeded, the URL of the file on Google
124 Code, None otherwise.
126 # The login is the user part of user@gmail.com. If the login provided
127 # is in the full user@domain form, strip it down.
128 if '@' in user_name:
129 user_name = user_name[:user_name.index('@')]
131 form_fields = [('summary', summary)]
132 if labels is not None:
133 form_fields.extend([('label', l.strip()) for l in labels])
135 content_type, body = encode_upload_request(form_fields, file)
137 upload_host = '%s.googlecode.com' % project_name
138 upload_uri = '/files'
139 auth_token = base64.b64encode('%s:%s'% (user_name, password))
140 headers = {
141 'Authorization': 'Basic %s' % auth_token,
142 'User-Agent': 'Googlecode.com uploader v0.9.4',
143 'Content-Type': content_type,
146 server = httplib.HTTPSConnection(upload_host)
147 server.request('POST', upload_uri, body, headers)
148 resp = server.getresponse()
149 server.close()
151 if resp.status == 201:
152 location = resp.getheader('Location', None)
153 else:
154 location = None
155 return resp.status, resp.reason, location
158 def encode_upload_request(fields, file_path):
159 """Encode the given fields and file into a multipart form body.
161 fields is a sequence of (name, value) pairs. file is the path of
162 the file to upload. The file will be uploaded to Google Code with
163 the same file name.
165 Returns: (content_type, body) ready for httplib.HTTP instance
167 BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
168 CRLF = '\r\n'
170 body = []
172 # Add the metadata about the upload first
173 for key, value in fields:
174 body.extend(
175 ['--' + BOUNDARY,
176 'Content-Disposition: form-data; name="%s"' % key,
178 value,
181 # Now add the file itself
182 file_name = os.path.basename(file_path)
183 f = open(file_path, 'rb')
184 file_content = f.read()
185 f.close()
187 body.extend(
188 ['--' + BOUNDARY,
189 'Content-Disposition: form-data; name="filename"; filename="%s"'
190 % file_name,
191 # The upload server determines the mime-type, no need to set it.
192 'Content-Type: application/octet-stream',
194 file_content,
197 # Finalize the form body
198 body.extend(['--' + BOUNDARY + '--', ''])
200 return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
203 def upload_find_auth(file_path, project_name, summary, labels=None,
204 config_dir=None, user_name=None, tries=3):
205 """Find credentials and upload a file to a Google Code project's file server.
207 file_path, project_name, summary, and labels are passed as-is to upload.
209 If config_dir is None, try get_svn_config_dir(); if it is 'none', skip
210 trying the Subversion configuration entirely. If user_name is not None, use
211 it for the first attempt; prompt for subsequent attempts.
213 Args:
214 file_path: The local path to the file.
215 project_name: The name of your project on Google Code.
216 summary: A small description for the file.
217 labels: an optional list of label strings with which to tag the file.
218 config_dir: Path to Subversion configuration directory, 'none', or None.
219 user_name: Your Google account name.
220 tries: How many attempts to make.
223 if config_dir != 'none':
224 # Try to load username/password from svn config for first try.
225 if config_dir is None:
226 config_dir = get_svn_config_dir()
227 (svn_username, password) = get_svn_auth(project_name, config_dir)
228 if user_name is None:
229 # If username was not supplied by caller, use svn config.
230 user_name = svn_username
231 else:
232 # Just initialize password for the first try.
233 password = None
235 while tries > 0:
236 if user_name is None:
237 # Read username if not specified or loaded from svn config, or on
238 # subsequent tries.
239 sys.stdout.write('Please enter your googlecode.com username: ')
240 sys.stdout.flush()
241 user_name = sys.stdin.readline().rstrip()
242 if password is None:
243 # Read password if not loaded from svn config, or on subsequent tries.
244 print 'Please enter your googlecode.com password.'
245 print '** Note that this is NOT your Gmail account password! **'
246 print 'It is the password you use to access Subversion repositories,'
247 print 'and can be found here: http://code.google.com/hosting/settings'
248 password = getpass.getpass()
250 status, reason, url = upload(file_path, project_name, user_name, password,
251 summary, labels)
252 # Returns 403 Forbidden instead of 401 Unauthorized for bad
253 # credentials as of 2007-07-17.
254 if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
255 # Rest for another try.
256 user_name = password = None
257 tries = tries - 1
258 else:
259 # We're done.
260 break
262 return status, reason, url
265 def main():
266 parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
267 '-p PROJECT [options] FILE')
268 parser.add_option('--config-dir', dest='config_dir', metavar='DIR',
269 help='read svn auth data from DIR'
270 ' ("none" means not to use svn auth data)')
271 parser.add_option('-s', '--summary', dest='summary',
272 help='Short description of the file')
273 parser.add_option('-p', '--project', dest='project',
274 help='Google Code project name')
275 parser.add_option('-u', '--user', dest='user',
276 help='Your Google Code username')
277 parser.add_option('-l', '--labels', dest='labels',
278 help='An optional list of labels to attach to the file')
280 options, args = parser.parse_args()
282 if not options.summary:
283 parser.error('File summary is missing.')
284 elif not options.project:
285 parser.error('Project name is missing.')
286 elif len(args) < 1:
287 parser.error('File to upload not provided.')
288 elif len(args) > 1:
289 parser.error('Only one file may be specified.')
291 file_path = args[0]
293 if options.labels:
294 labels = options.labels.split(',')
295 else:
296 labels = None
298 status, reason, url = upload_find_auth(file_path, options.project,
299 options.summary, labels,
300 options.config_dir, options.user)
301 if url:
302 print 'The file was uploaded successfully.'
303 print 'URL: %s' % url
304 return 0
305 else:
306 print 'An error occurred. Your file was not uploaded.'
307 print 'Google Code upload server said: %s (%s)' % (reason, status)
308 return 1
311 if __name__ == '__main__':
312 sys.exit(main())