1#!/usr/bin/env python
2#
3# Copyright 2006, 2007 Google Inc. All Rights Reserved.
4# Author: danderson@google.com (David Anderson)
5#
6# Script for uploading files to a Google Code project.
7#
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.
11#
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.
20#
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).
30#
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.
38#
39# Licensed under the terms of the Apache Software License 2.0:
40#  http://www.apache.org/licenses/LICENSE-2.0
41#
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
45
46"""Google Code file uploader script.
47"""
48
49__author__ = 'danderson@google.com (David Anderson)'
50
51import httplib
52import os.path
53import optparse
54import getpass
55import base64
56import sys
57
58
59def upload(file, project_name, user_name, password, summary, labels=None):
60  """Upload a file to a Google Code project's file server.
61
62  Args:
63    file: The local path to the file.
64    project_name: The name of your project on Google Code.
65    user_name: Your Google account name.
66    password: The googlecode.com password for your account.
67              Note that this is NOT your global Google Account password!
68    summary: A small description for the file.
69    labels: an optional list of label strings with which to tag the file.
70
71  Returns: a tuple:
72    http_status: 201 if the upload succeeded, something else if an
73                 error occured.
74    http_reason: The human-readable string associated with http_status
75    file_url: If the upload succeeded, the URL of the file on Google
76              Code, None otherwise.
77  """
78  # The login is the user part of user@gmail.com. If the login provided
79  # is in the full user@domain form, strip it down.
80  if user_name.endswith('@gmail.com'):
81    user_name = user_name[:user_name.index('@gmail.com')]
82
83  form_fields = [('summary', summary)]
84  if labels is not None:
85    form_fields.extend([('label', l.strip()) for l in labels])
86
87  content_type, body = encode_upload_request(form_fields, file)
88
89  upload_host = '%s.googlecode.com' % project_name
90  upload_uri = '/files'
91  auth_token = base64.b64encode('%s:%s'% (user_name, password))
92  headers = {
93    'Authorization': 'Basic %s' % auth_token,
94    'User-Agent': 'Googlecode.com uploader v0.9.4',
95    'Content-Type': content_type,
96    }
97
98  server = httplib.HTTPSConnection(upload_host)
99  server.request('POST', upload_uri, body, headers)
100  resp = server.getresponse()
101  server.close()
102
103  if resp.status == 201:
104    location = resp.getheader('Location', None)
105  else:
106    location = None
107  return resp.status, resp.reason, location
108
109
110def encode_upload_request(fields, file_path):
111  """Encode the given fields and file into a multipart form body.
112
113  fields is a sequence of (name, value) pairs. file is the path of
114  the file to upload. The file will be uploaded to Google Code with
115  the same file name.
116
117  Returns: (content_type, body) ready for httplib.HTTP instance
118  """
119  BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
120  CRLF = '\r\n'
121
122  body = []
123
124  # Add the metadata about the upload first
125  for key, value in fields:
126    body.extend(
127      ['--' + BOUNDARY,
128       'Content-Disposition: form-data; name="%s"' % key,
129       '',
130       value,
131       ])
132
133  # Now add the file itself
134  file_name = os.path.basename(file_path)
135  f = open(file_path, 'rb')
136  file_content = f.read()
137  f.close()
138
139  body.extend(
140    ['--' + BOUNDARY,
141     'Content-Disposition: form-data; name="filename"; filename="%s"'
142     % file_name,
143     # The upload server determines the mime-type, no need to set it.
144     'Content-Type: application/octet-stream',
145     '',
146     file_content,
147     ])
148
149  # Finalize the form body
150  body.extend(['--' + BOUNDARY + '--', ''])
151
152  return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
153
154
155def upload_find_auth(file_path, project_name, summary, labels=None,
156                     user_name=None, password=None, tries=3):
157  """Find credentials and upload a file to a Google Code project's file server.
158
159  file_path, project_name, summary, and labels are passed as-is to upload.
160
161  Args:
162    file_path: The local path to the file.
163    project_name: The name of your project on Google Code.
164    summary: A small description for the file.
165    labels: an optional list of label strings with which to tag the file.
166    config_dir: Path to Subversion configuration directory, 'none', or None.
167    user_name: Your Google account name.
168    tries: How many attempts to make.
169  """
170  if user_name is None or password is None:
171    from netrc import netrc
172    authenticators = netrc().authenticators("code.google.com")
173    if authenticators:
174      if user_name is None:
175        user_name = authenticators[0]
176      if password is None:
177        password = authenticators[2]
178
179  while tries > 0:
180    if user_name is None:
181      # Read username if not specified or loaded from svn config, or on
182      # subsequent tries.
183      sys.stdout.write('Please enter your googlecode.com username: ')
184      sys.stdout.flush()
185      user_name = sys.stdin.readline().rstrip()
186    if password is None:
187      # Read password if not loaded from svn config, or on subsequent tries.
188      print 'Please enter your googlecode.com password.'
189      print '** Note that this is NOT your Gmail account password! **'
190      print 'It is the password you use to access Subversion repositories,'
191      print 'and can be found here: http://code.google.com/hosting/settings'
192      password = getpass.getpass()
193
194    status, reason, url = upload(file_path, project_name, user_name, password,
195                                 summary, labels)
196    # Returns 403 Forbidden instead of 401 Unauthorized for bad
197    # credentials as of 2007-07-17.
198    if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
199      # Rest for another try.
200      user_name = password = None
201      tries = tries - 1
202    else:
203      # We're done.
204      break
205
206  return status, reason, url
207
208
209def main():
210  parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
211                                 '-p PROJECT [options] FILE')
212  parser.add_option('-s', '--summary', dest='summary',
213                    help='Short description of the file')
214  parser.add_option('-p', '--project', dest='project',
215                    help='Google Code project name')
216  parser.add_option('-u', '--user', dest='user',
217                    help='Your Google Code username')
218  parser.add_option('-w', '--password', dest='password',
219                    help='Your Google Code password')
220  parser.add_option('-l', '--labels', dest='labels',
221                    help='An optional list of comma-separated labels to attach '
222                    'to the file')
223
224  options, args = parser.parse_args()
225
226  if not options.summary:
227    parser.error('File summary is missing.')
228  elif not options.project:
229    parser.error('Project name is missing.')
230  elif len(args) < 1:
231    parser.error('File to upload not provided.')
232  elif len(args) > 1:
233    parser.error('Only one file may be specified.')
234
235  file_path = args[0]
236
237  if options.labels:
238    labels = options.labels.split(',')
239  else:
240    labels = None
241
242  status, reason, url = upload_find_auth(file_path, options.project,
243                                         options.summary, labels,
244                                         options.user, options.password)
245  if url:
246    print 'The file was uploaded successfully.'
247    print 'URL: %s' % url
248    return 0
249  else:
250    print 'An error occurred. Your file was not uploaded.'
251    print 'Google Code upload server said: %s (%s)' % (reason, status)
252    return 1
253
254
255if __name__ == '__main__':
256  sys.exit(main())
257