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 occurred.
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
171  while tries > 0:
172    if user_name is None:
173      # Read username if not specified or loaded from svn config, or on
174      # subsequent tries.
175      sys.stdout.write('Please enter your googlecode.com username: ')
176      sys.stdout.flush()
177      user_name = sys.stdin.readline().rstrip()
178    if password is None:
179      # Read password if not loaded from svn config, or on subsequent tries.
180      print 'Please enter your googlecode.com password.'
181      print '** Note that this is NOT your Gmail account password! **'
182      print 'It is the password you use to access Subversion repositories,'
183      print 'and can be found here: http://code.google.com/hosting/settings'
184      password = getpass.getpass()
185
186    status, reason, url = upload(file_path, project_name, user_name, password,
187                                 summary, labels)
188    # Returns 403 Forbidden instead of 401 Unauthorized for bad
189    # credentials as of 2007-07-17.
190    if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
191      # Rest for another try.
192      user_name = password = None
193      tries = tries - 1
194    else:
195      # We're done.
196      break
197
198  return status, reason, url
199
200
201def main():
202  parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
203                                 '-p PROJECT [options] FILE')
204  parser.add_option('-s', '--summary', dest='summary',
205                    help='Short description of the file')
206  parser.add_option('-p', '--project', dest='project',
207                    help='Google Code project name')
208  parser.add_option('-u', '--user', dest='user',
209                    help='Your Google Code username')
210  parser.add_option('-w', '--password', dest='password',
211                    help='Your Google Code password')
212  parser.add_option('-l', '--labels', dest='labels',
213                    help='An optional list of comma-separated labels to attach '
214                    'to the file')
215
216  options, args = parser.parse_args()
217
218  if not options.summary:
219    parser.error('File summary is missing.')
220  elif not options.project:
221    parser.error('Project name is missing.')
222  elif len(args) < 1:
223    parser.error('File to upload not provided.')
224  elif len(args) > 1:
225    parser.error('Only one file may be specified.')
226
227  file_path = args[0]
228
229  if options.labels:
230    labels = options.labels.split(',')
231  else:
232    labels = None
233
234  status, reason, url = upload_find_auth(file_path, options.project,
235                                         options.summary, labels,
236                                         options.user, options.password)
237  if url:
238    print 'The file was uploaded successfully.'
239    print 'URL: %s' % url
240    return 0
241  else:
242    print 'An error occurred. Your file was not uploaded.'
243    print 'Google Code upload server said: %s (%s)' % (reason, status)
244    return 1
245
246
247if __name__ == '__main__':
248  sys.exit(main())
249